From 92f567f69fe27c6a428805513c99415fe613d79a Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 13 Dec 2022 13:06:44 +0000 Subject: [PATCH 1/3] Most of explicit and lazy loading for no-tracking queries Part of #10042 Current limitations are: - The lazy-loading delegate injection doesn't support tracking of when an navigation is loaded or not; I have an idea for this - There is no identity resolution when the entities are not tracked, even if the query behavior is NoTrackingWithIdentityResolution - The model should validated for only a single service property for a given type I will submit PRs and/or file issues for these things. --- .../Infrastructure/ILazyLoader.cs | 11 + .../Internal/InternalEntityEntry.cs | 54 +- .../Diagnostics/CoreLoggerExtensions.cs | 4 +- .../Diagnostics/LazyLoadingEventData.cs | 4 +- .../Infrastructure/Internal/LazyLoader.cs | 130 +- src/EFCore/Internal/EntityFinder.cs | 120 +- src/EFCore/Internal/IInjectableService.cs | 54 + src/EFCore/Internal/ManyToManyLoader.cs | 78 +- .../ServicePropertyDiscoveryConvention.cs | 34 +- .../Metadata/Internal/IMemberClassifier.cs | 2 +- .../Metadata/Internal/MemberClassifier.cs | 12 +- src/EFCore/Properties/CoreStrings.Designer.cs | 8 +- src/EFCore/Properties/CoreStrings.resx | 6 +- src/Shared/PropertyInfoExtensions.cs | 9 +- .../Query/WarningsTest.cs | 2 + .../F1FixtureBase.cs | 5 +- .../FieldsOnlyLoadTestBase.cs | 250 +- .../LazyLoadProxyTestBase.cs | 1151 ++-- .../LoadTestBase.cs | 4666 ++++++-------- .../ManyToManyFieldsLoadTestBase.cs | 26 +- .../ManyToManyLoadTestBase.cs | 413 +- .../UnidirectionalManyToManyLoadTestBase.cs | 28 +- .../WithConstructorsTestBase.cs | 35 +- .../LazyLoadTestBase.cs | 5425 +++++++++++++++++ .../LazyLoadProxySqlServerTest.cs | 102 +- .../LoadSqlServerTest.cs | 640 +- .../ServicePropertyDiscoveryConventionTest.cs | 58 +- 27 files changed, 9364 insertions(+), 3963 deletions(-) create mode 100644 src/EFCore/Internal/IInjectableService.cs create mode 100644 test/EFCore.Specification.Tests/test/EFCore.Specification.Tests/LazyLoadTestBase.cs diff --git a/src/EFCore.Abstractions/Infrastructure/ILazyLoader.cs b/src/EFCore.Abstractions/Infrastructure/ILazyLoader.cs index c6d14bca713..ad0ec5aa5ca 100644 --- a/src/EFCore.Abstractions/Infrastructure/ILazyLoader.cs +++ b/src/EFCore.Abstractions/Infrastructure/ILazyLoader.cs @@ -34,6 +34,17 @@ void SetLoaded( [CallerMemberName] string navigationName = "", bool loaded = true); + /// + /// Gets whether or not the given navigation as known to be completely loaded or known to be + /// no longer completely loaded. + /// + /// The entity on which the navigation property is located. + /// The navigation property name. + /// if the navigation is known to be loaded. + bool IsLoaded( + object entity, + [CallerMemberName] string navigationName = ""); + /// /// Loads a navigation property if it has not already been loaded. /// diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 7e235b36e0d..ad84019646b 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -474,22 +474,23 @@ private void SetServiceProperties(EntityState oldState, EntityState newState) { foreach (var serviceProperty in EntityType.GetServiceProperties()) { - this[serviceProperty] - = serviceProperty + this[serviceProperty] = (this[serviceProperty] is IInjectableService injectableService + ? injectableService.Attaching(Context, Entity, injectableService) + : null) + ?? serviceProperty .ParameterBinding - .ServiceDelegate( - new MaterializationContext( - ValueBuffer.Empty, - Context), - EntityType, - Entity); + .ServiceDelegate(new MaterializationContext(ValueBuffer.Empty, Context), EntityType, Entity); } } else if (newState == EntityState.Detached) { foreach (var serviceProperty in EntityType.GetServiceProperties()) { - this[serviceProperty] = null; + if (!(this[serviceProperty] is IInjectableService detachable) + || detachable.Detaching(Context, Entity)) + { + this[serviceProperty] = null; + } } } } @@ -846,7 +847,8 @@ private T ReadShadowValue(int shadowIndex) private static readonly MethodInfo ReadOriginalValueMethod = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadOriginalValue))!; - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", + [UnconditionalSuppressMessage( + "ReflectionAnalysis", "IL2060", Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")] internal static MethodInfo MakeReadOriginalValueMethod(Type type) => ReadOriginalValueMethod.MakeGenericMethod(type); @@ -858,7 +860,8 @@ private T ReadOriginalValue(IProperty property, int originalValueIndex) private static readonly MethodInfo ReadRelationshipSnapshotValueMethod = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadRelationshipSnapshotValue))!; - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", + [UnconditionalSuppressMessage( + "ReflectionAnalysis", "IL2060", Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")] internal static MethodInfo MakeReadRelationshipSnapshotValueMethod(Type type) => ReadRelationshipSnapshotValueMethod.MakeGenericMethod(type); @@ -867,7 +870,8 @@ internal static MethodInfo MakeReadRelationshipSnapshotValueMethod(Type type) private T ReadRelationshipSnapshotValue(IPropertyBase propertyBase, int relationshipSnapshotIndex) => _relationshipsSnapshot.GetValue(this, propertyBase, relationshipSnapshotIndex); - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", + [UnconditionalSuppressMessage( + "ReflectionAnalysis", "IL2060", Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")] internal static MethodInfo MakeReadStoreGeneratedValueMethod(Type type) => ReadStoreGeneratedValueMethod.MakeGenericMethod(type); @@ -882,7 +886,8 @@ private T ReadStoreGeneratedValue(int storeGeneratedIndex) private static readonly MethodInfo ReadTemporaryValueMethod = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadTemporaryValue))!; - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", + [UnconditionalSuppressMessage( + "ReflectionAnalysis", "IL2060", Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")] internal static MethodInfo MakeReadTemporaryValueMethod(Type type) => ReadTemporaryValueMethod.MakeGenericMethod(type); @@ -895,7 +900,8 @@ private static readonly MethodInfo GetCurrentValueMethod = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethods(nameof(GetCurrentValue)).Single( m => m.IsGenericMethod); - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060", + [UnconditionalSuppressMessage( + "ReflectionAnalysis", "IL2060", Justification = "MakeGenericMethod wrapper, see https://github.com/dotnet/linker/issues/2482")] internal static MethodInfo MakeGetCurrentValueMethod(Type type) => GetCurrentValueMethod.MakeGenericMethod(type); @@ -2001,11 +2007,14 @@ public void SetIsLoaded(INavigationBase navigation, bool loaded = true) CoreStrings.ReferenceMustBeLoaded(navigation.Name, navigation.DeclaringEntityType.DisplayName())); } - _stateData.FlagProperty(navigation.GetIndex(), PropertyFlag.IsLoaded, isFlagged: loaded); - - foreach (var lazyLoaderProperty in EntityType.GetServiceProperties().Where(p => p.ClrType == typeof(ILazyLoader))) + var lazyLoader = GetLazyLoader(); + if (lazyLoader != null) { - ((ILazyLoader?)this[lazyLoaderProperty])?.SetLoaded(Entity, navigation.Name, loaded); + lazyLoader.SetLoaded(Entity, navigation.Name, loaded); + } + else + { + _stateData.FlagProperty(navigation.GetIndex(), PropertyFlag.IsLoaded, isFlagged: loaded); } } @@ -2016,7 +2025,14 @@ public void SetIsLoaded(INavigationBase navigation, bool loaded = true) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public bool IsLoaded(INavigationBase navigation) - => _stateData.IsPropertyFlagged(navigation.GetIndex(), PropertyFlag.IsLoaded); + => GetLazyLoader()?.IsLoaded(Entity, navigation.Name) + ?? _stateData.IsPropertyFlagged(navigation.GetIndex(), PropertyFlag.IsLoaded); + + private ILazyLoader? GetLazyLoader() + { + var lazyLoaderProperty = EntityType.GetServiceProperties().FirstOrDefault(p => p.ClrType == typeof(ILazyLoader)); + return lazyLoaderProperty != null ? (ILazyLoader?)this[lazyLoaderProperty] : null; + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs index e41f0fb569c..04a1f0eba81 100644 --- a/src/EFCore/Diagnostics/CoreLoggerExtensions.cs +++ b/src/EFCore/Diagnostics/CoreLoggerExtensions.cs @@ -1108,7 +1108,7 @@ private static string ExecutionStrategyRetrying(EventDefinitionBase definition, /// The name of the navigation property. public static void LazyLoadOnDisposedContextWarning( this IDiagnosticsLogger diagnostics, - DbContext context, + DbContext? context, object entityType, string navigationName) { @@ -1188,7 +1188,7 @@ private static string NavigationLazyLoading(EventDefinitionBase definition, Even /// The name of the navigation property. public static void DetachedLazyLoadingWarning( this IDiagnosticsLogger diagnostics, - DbContext context, + DbContext? context, object entityType, string navigationName) { diff --git a/src/EFCore/Diagnostics/LazyLoadingEventData.cs b/src/EFCore/Diagnostics/LazyLoadingEventData.cs index 3135451a0cd..4610d29e2a7 100644 --- a/src/EFCore/Diagnostics/LazyLoadingEventData.cs +++ b/src/EFCore/Diagnostics/LazyLoadingEventData.cs @@ -16,13 +16,13 @@ public class LazyLoadingEventData : DbContextEventData /// /// The event definition. /// A delegate that generates a log message for this event. - /// The current . + /// The current , or if it is no longer available. /// The entity instance on which lazy loading was initiated. /// The navigation property name of the relationship to be loaded. public LazyLoadingEventData( EventDefinitionBase eventDefinition, Func messageGenerator, - DbContext context, + DbContext? context, object entity, string navigationPropertyName) : base(eventDefinition, messageGenerator, context) diff --git a/src/EFCore/Infrastructure/Internal/LazyLoader.cs b/src/EFCore/Infrastructure/Internal/LazyLoader.cs index f8bfc0b3302..b75c3bb19f4 100644 --- a/src/EFCore/Infrastructure/Internal/LazyLoader.cs +++ b/src/EFCore/Infrastructure/Internal/LazyLoader.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal; @@ -12,10 +13,12 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure.Internal; /// 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 class LazyLoader : ILazyLoader +public class LazyLoader : ILazyLoader, IInjectableService { private bool _disposed; + private bool _detached; private IDictionary? _loadedStates; + private List<(object Entity, string NavigationName)>? _isLoading; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -47,6 +50,17 @@ public virtual void SetLoaded( _loadedStates[navigationName] = loaded; } + /// + /// 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. + /// + public virtual bool IsLoaded(object entity, string navigationName = "") + => _loadedStates != null + && _loadedStates.TryGetValue(navigationName, out var loaded) + && loaded; + /// /// 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 @@ -61,7 +75,7 @@ public virtual void SetLoaded( /// 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. /// - protected virtual DbContext Context { get; } + protected virtual DbContext? Context { get; set; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -75,16 +89,28 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName Check.NotNull(entity, nameof(entity)); Check.NotEmpty(navigationName, nameof(navigationName)); - if (ShouldLoad(entity, navigationName, out var entry)) + var navEntry = (entity, navigationName); + if (!(_isLoading ??= new List<(object Entity, string NavigationName)>()).Contains(navEntry)) { try { - entry.Load(); + _isLoading.Add(navEntry); + if (ShouldLoad(entity, navigationName, out var entry)) + { + try + { + entry.Load(); + } + catch + { + entry.IsLoaded = false; + throw; + } + } } - catch + finally { - SetLoaded(entity, navigationName, false); - throw; + _isLoading.Remove(navEntry); } } } @@ -103,53 +129,49 @@ public virtual async Task LoadAsync( Check.NotNull(entity, nameof(entity)); Check.NotEmpty(navigationName, nameof(navigationName)); - if (ShouldLoad(entity, navigationName, out var entry)) + var navEntry = (entity, navigationName); + if (!(_isLoading ??= new List<(object Entity, string NavigationName)>()).Contains(navEntry)) { try { - await entry.LoadAsync(cancellationToken).ConfigureAwait(false); + _isLoading.Add(navEntry); + if (ShouldLoad(entity, navigationName, out var entry)) + { + try + { + await entry.LoadAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + entry.IsLoaded = false; + throw; + } + } } - catch + finally { - SetLoaded(entity, navigationName, false); - throw; + _isLoading.Remove(navEntry); } } } private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)] out NavigationEntry? navigationEntry) { - if (_loadedStates != null - && _loadedStates.TryGetValue(navigationName, out var loaded) - && loaded) + if (!_detached && !IsLoaded(entity, navigationName)) { - navigationEntry = null; - return false; - } - - if (_disposed) - { - Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName); - } - else if (Context.ChangeTracker.LazyLoadingEnabled) - { - // Set early to avoid recursive loading overflow - SetLoaded(entity, navigationName, loaded: true); - - var entityEntry = Context.Entry(entity); // Will use local-DetectChanges, if enabled. - var tempNavigationEntry = entityEntry.Navigation(navigationName); - - if (entityEntry.State == EntityState.Detached) + if (_disposed) { - Logger.DetachedLazyLoadingWarning(Context, entity, navigationName); + Logger.LazyLoadOnDisposedContextWarning(Context, entity, navigationName); } - else if (!tempNavigationEntry.IsLoaded) + else if (Context!.ChangeTracker.LazyLoadingEnabled) // Check again because the nav may be loaded without the loader knowing { - Logger.NavigationLazyLoading(Context, entity, navigationName); - - navigationEntry = tempNavigationEntry; + navigationEntry = Context.Entry(entity).Navigation(navigationName); // Will use local-DetectChanges, if enabled. + if (!navigationEntry.IsLoaded) + { + Logger.NavigationLazyLoading(Context, entity, navigationName); - return true; + return true; + } } } @@ -164,5 +186,35 @@ private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual void Dispose() - => _disposed = true; + { + Context = null; + _disposed = true; + } + + /// + /// 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. + /// + public virtual bool Detaching(DbContext context, object entity) + { + _detached = true; + Dispose(); + return false; + } + + /// + /// 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. + /// + public virtual IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService) + { + _disposed = false; + _detached = false; + Context = context; + return this; + } } diff --git a/src/EFCore/Internal/EntityFinder.cs b/src/EFCore/Internal/EntityFinder.cs index c54c7d9751d..e74ab47e506 100644 --- a/src/EFCore/Internal/EntityFinder.cs +++ b/src/EFCore/Internal/EntityFinder.cs @@ -419,16 +419,37 @@ private bool TryGetByForeignKey( /// public virtual void Load(INavigation navigation, InternalEntityEntry entry) { - if (entry.EntityState == EntityState.Detached) - { - throw new InvalidOperationException(CoreStrings.CannotLoadDetached(navigation.Name, entry.EntityType.DisplayName())); - } - var keyValues = GetLoadValues(navigation, entry); // Short-circuit for any null key values for perf and because of #6129 if (keyValues != null) { - Query(navigation, keyValues).Load(); + var queryable = Query(navigation, keyValues, entry); + if (entry.EntityState == EntityState.Detached) + { + var inverse = navigation.Inverse; + if (navigation.IsCollection) + { + foreach (var loaded in queryable) + { + Fixup(entry, navigation, inverse, loaded); + } + } + else + { + Fixup(entry, navigation, inverse, queryable.FirstOrDefault()); + } + } + else + { + if (navigation.IsCollection) + { + queryable.Load(); + } + else + { + _ = queryable.FirstOrDefault(); + } + } } entry.SetIsLoaded(navigation); @@ -445,22 +466,69 @@ public virtual async Task LoadAsync( InternalEntityEntry entry, CancellationToken cancellationToken = default) { - if (entry.EntityState == EntityState.Detached) - { - throw new InvalidOperationException(CoreStrings.CannotLoadDetached(navigation.Name, entry.EntityType.DisplayName())); - } - - // Short-circuit for any null key values for perf and because of #6129 var keyValues = GetLoadValues(navigation, entry); + // Short-circuit for any null key values for perf and because of #6129 if (keyValues != null) { - await Query(navigation, keyValues).LoadAsync(cancellationToken) - .ConfigureAwait(false); + var queryable = Query(navigation, keyValues, entry); + if (entry.EntityState == EntityState.Detached) + { + var inverse = navigation.Inverse; + if (navigation.IsCollection) + { + await foreach (var loaded in queryable.AsAsyncEnumerable().WithCancellation(cancellationToken).ConfigureAwait(false)) + { + Fixup(entry, navigation, inverse, loaded); + } + } + else + { + Fixup( + entry, navigation, inverse, + await queryable.FirstOrDefaultAsync(cancellationToken: cancellationToken).ConfigureAwait(false)); + } + } + else + { + if (navigation.IsCollection) + { + await Query(navigation, keyValues, entry).LoadAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _ = await queryable.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + } + } } entry.SetIsLoaded(navigation); } + private void Fixup(InternalEntityEntry parentEntry, INavigation beingLoaded, INavigation? inverse, object? loaded) + { + SetValue(parentEntry, beingLoaded, loaded); + + if (inverse != null && loaded != null) + { + SetValue(_stateManager.GetOrCreateEntry(loaded), inverse, parentEntry.Entity); + } + + void SetValue(InternalEntityEntry entry, INavigation navigation, object? value) + { + if (navigation.IsCollection) + { + if (value != null) + { + entry.AddToCollection(navigation, value, forMaterialization: true); + } + } + else + { + entry.SetProperty(navigation, value, isMaterialization: true, setModified: false); + } + } + } + /// /// 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 @@ -469,11 +537,6 @@ await Query(navigation, keyValues).LoadAsync(cancellationToken) /// public virtual IQueryable Query(INavigation navigation, InternalEntityEntry entry) { - if (entry.EntityState == EntityState.Detached) - { - throw new InvalidOperationException(CoreStrings.CannotLoadDetached(navigation.Name, entry.EntityType.DisplayName())); - } - var keyValues = GetLoadValues(navigation, entry); // Short-circuit for any null key values for perf and because of #6129 if (keyValues == null) @@ -483,7 +546,7 @@ public virtual IQueryable Query(INavigation navigation, InternalEntityE return _queryRoot.Where(e => false); } - return Query(navigation, keyValues); + return Query(navigation, keyValues, entry); } /// @@ -528,8 +591,11 @@ public virtual IQueryable Query(INavigation navigation, InternalEntityE .Select(BuildProjection(entityType)); } - private IQueryable Query(INavigation navigation, object[] keyValues) - => _queryRoot.Where(BuildLambda(GetLoadProperties(navigation), new ValueBuffer(keyValues))).AsTracking(); + private IQueryable Query(INavigation navigation, object[] keyValues, InternalEntityEntry entry) + { + var queryable = _queryRoot.Where(BuildLambda(GetLoadProperties(navigation), new ValueBuffer(keyValues))); + return entry.EntityState == EntityState.Detached ? queryable.AsNoTrackingWithIdentityResolution() : queryable.AsTracking(); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -547,10 +613,18 @@ IQueryable IEntityFinder.Query(INavigation navigation, InternalEntityEntry entry : navigation.ForeignKey.PrincipalKey.Properties; var values = new object[properties.Count]; + var detached = entry.EntityState == EntityState.Detached; for (var i = 0; i < values.Length; i++) { - var value = entry[properties[i]]; + var property = properties[i]; + if (property.IsShadowProperty() && (detached || entry.IsUnknown(property))) + { + throw new InvalidOperationException( + CoreStrings.CannotLoadDetachedShadow(navigation.Name, entry.EntityType.DisplayName())); + } + + var value = entry[property]; if (value == null) { return null; diff --git a/src/EFCore/Internal/IInjectableService.cs b/src/EFCore/Internal/IInjectableService.cs new file mode 100644 index 00000000000..a3c3e86d890 --- /dev/null +++ b/src/EFCore/Internal/IInjectableService.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Internal; + +/// +/// +/// 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. +/// +/// +/// Implemented by service property types to notify services instances of lifecycle changes. +/// +/// +public interface IInjectableService +{ + /// + /// + /// 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. + /// + /// + /// Called when the entity holding this service is being detached from a DbContext. + /// + /// + /// The instance. + /// The entity instance that is being detached. + /// if the service property should be set to . + bool Detaching(DbContext context, object entity); + + /// + /// + /// 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. + /// + /// + /// Called when an entity that needs this is being attached to a DbContext. + /// + /// + /// The instance. + /// The entity instance that is being attached. + /// + /// The existing instance of this service being held by the entity instance, or if there + /// is no existing instance. + /// + /// The service instance to use, or if a new instance should be created. + IInjectableService? Attaching(DbContext context, object entity, IInjectableService? existingService); +} diff --git a/src/EFCore/Internal/ManyToManyLoader.cs b/src/EFCore/Internal/ManyToManyLoader.cs index 55f819eea51..dfd9a1caa01 100644 --- a/src/EFCore/Internal/ManyToManyLoader.cs +++ b/src/EFCore/Internal/ManyToManyLoader.cs @@ -17,6 +17,7 @@ public class ManyToManyLoader : ICollectionLoader /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -27,6 +28,7 @@ public class ManyToManyLoader : ICollectionLoader @@ -42,7 +44,20 @@ public virtual void Load(InternalEntityEntry entry) // Short-circuit for any null key values for perf and because of #6129 if (keyValues != null) { - Query(entry.Context, keyValues).Load(); + var queryable = Query(entry.Context, keyValues, entry); + + if (entry.EntityState == EntityState.Detached) + { + var inverse = _skipNavigation.Inverse; + foreach (var loaded in queryable) + { + Fixup(entry, loaded); + } + } + else + { + queryable.Load(); + } } entry.SetIsLoaded(_skipNavigation); @@ -61,12 +76,43 @@ public virtual async Task LoadAsync(InternalEntityEntry entry, CancellationToken // Short-circuit for any null key values for perf and because of #6129 if (keyValues != null) { - await Query(entry.Context, keyValues).LoadAsync(cancellationToken).ConfigureAwait(false); + var queryable = Query(entry.Context, keyValues, entry); + + if (entry.EntityState == EntityState.Detached) + { + var inverse = _skipNavigation.Inverse; + foreach (var loaded in queryable) + { + Fixup(entry, loaded); + } + } + else + { + await queryable.LoadAsync(cancellationToken).ConfigureAwait(false); + } } entry.SetIsLoaded(_skipNavigation); } + private void Fixup(InternalEntityEntry parentEntry, object? loaded) + { + SetValue(parentEntry, _skipNavigation, loaded); + + if (_inverseSkipNavigation != null && loaded != null) + { + SetValue(parentEntry.StateManager.GetOrCreateEntry(loaded), _inverseSkipNavigation, parentEntry.Entity); + } + + void SetValue(InternalEntityEntry entry, ISkipNavigation navigation, object? value) + { + if (value != null) + { + entry.AddToCollection(navigation, value, forMaterialization: true); + } + } + } + /// /// 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 @@ -89,20 +135,24 @@ public virtual IQueryable Query(InternalEntityEntry entry) return queryRoot.Where(e => false); } - return Query(context, keyValues); + return Query(context, keyValues, entry); } private object[]? PrepareForLoad(InternalEntityEntry entry) { - if (entry.EntityState == EntityState.Detached) - { - throw new InvalidOperationException(CoreStrings.CannotLoadDetached(_skipNavigation.Name, entry.EntityType.DisplayName())); - } - var properties = _skipNavigation.ForeignKey.PrincipalKey.Properties; var values = new object[properties.Count]; + var detached = entry.EntityState == EntityState.Detached; + for (var i = 0; i < values.Length; i++) { + var property = properties[i]; + if (property.IsShadowProperty() && (detached || entry.IsUnknown(property))) + { + throw new InvalidOperationException( + CoreStrings.CannotLoadDetachedShadow(_skipNavigation.Name, entry.EntityType.DisplayName())); + } + var value = entry[properties[i]]; if (value == null) { @@ -124,9 +174,7 @@ public virtual IQueryable Query(InternalEntityEntry entry) IQueryable ICollectionLoader.Query(InternalEntityEntry entry) => Query(entry); - private IQueryable Query( - DbContext context, - object[] keyValues) + private IQueryable Query(DbContext context, object[] keyValues, InternalEntityEntry entry) { var loadProperties = _skipNavigation.ForeignKey.PrincipalKey.Properties; @@ -143,12 +191,12 @@ private IQueryable Query( ? context.Set(_skipNavigation.DeclaringEntityType.Name) : context.Set(); - return queryRoot - .AsTracking() + var queryable = queryRoot .Where(BuildWhereLambda(loadProperties, new ValueBuffer(keyValues))) .SelectMany(BuildSelectManyLambda(_skipNavigation)) - .NotQuiteInclude(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))) - .AsQueryable(); + .NotQuiteInclude(BuildIncludeLambda(_skipNavigation.Inverse, loadProperties, new ValueBuffer(keyValues))); + + return entry.EntityState == EntityState.Detached ? queryable.AsNoTrackingWithIdentityResolution() : queryable.AsTracking(); } private static Expression>> BuildIncludeLambda( diff --git a/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs b/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs index 32851190b7a..8ad9ab723bb 100644 --- a/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs +++ b/src/EFCore/Metadata/Conventions/ServicePropertyDiscoveryConvention.cs @@ -62,21 +62,33 @@ private void Process(IConventionEntityTypeBuilder entityTypeBuilder) { var entityType = entityTypeBuilder.Metadata; var model = entityType.Model; - foreach (var propertyInfo in entityType.GetRuntimeProperties().Values) + DiscoverServiceProperties(entityType.GetRuntimeProperties().Values); + DiscoverServiceProperties(entityType.GetRuntimeFields().Values); + + void DiscoverServiceProperties(IEnumerable members) { - if (!entityTypeBuilder.CanHaveServiceProperty(propertyInfo)) + foreach (var memberInfo in members) { - continue; - } + if (!entityTypeBuilder.CanHaveServiceProperty(memberInfo)) + { + continue; + } - var factory = Dependencies.MemberClassifier.FindServicePropertyCandidateBindingFactory(propertyInfo, model); - if (factory == null) - { - continue; - } + var factory = Dependencies.MemberClassifier.FindServicePropertyCandidateBindingFactory(memberInfo, model); + if (factory == null) + { + continue; + } + + var memberType = memberInfo.GetMemberType(); + if (entityType.GetServiceProperties().Any(p => p.ClrType == memberType)) + { + continue; + } - entityTypeBuilder.ServiceProperty(propertyInfo)?.HasParameterBinding( - (ServiceParameterBinding)factory.Bind(entityType, propertyInfo.PropertyType, propertyInfo.GetSimpleMemberName())); + entityTypeBuilder.ServiceProperty(memberInfo)?.HasParameterBinding( + (ServiceParameterBinding)factory.Bind(entityType, memberType, memberInfo.GetSimpleMemberName())); + } } } } diff --git a/src/EFCore/Metadata/Internal/IMemberClassifier.cs b/src/EFCore/Metadata/Internal/IMemberClassifier.cs index 56038a75e06..6256195145f 100644 --- a/src/EFCore/Metadata/Internal/IMemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/IMemberClassifier.cs @@ -56,5 +56,5 @@ public interface IMemberClassifier /// 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. /// - IParameterBindingFactory? FindServicePropertyCandidateBindingFactory(PropertyInfo propertyInfo, IConventionModel model); + IParameterBindingFactory? FindServicePropertyCandidateBindingFactory(MemberInfo memberInfo, IConventionModel model); } diff --git a/src/EFCore/Metadata/Internal/MemberClassifier.cs b/src/EFCore/Metadata/Internal/MemberClassifier.cs index f2d23b5bfdd..16e44d695f9 100644 --- a/src/EFCore/Metadata/Internal/MemberClassifier.cs +++ b/src/EFCore/Metadata/Internal/MemberClassifier.cs @@ -186,15 +186,15 @@ public virtual bool IsCandidatePrimitiveProperty(PropertyInfo propertyInfo, ICon /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual IParameterBindingFactory? FindServicePropertyCandidateBindingFactory( - PropertyInfo propertyInfo, + MemberInfo memberInfo, IConventionModel model) { - if (!propertyInfo.IsCandidateProperty(publicOnly: false)) + if (!memberInfo.IsCandidateProperty(publicOnly: false)) { return null; } - var type = propertyInfo.PropertyType; + var type = memberInfo.GetMemberType(); var configurationType = ((Model)model).Configuration?.GetConfigurationType(type); if (configurationType != TypeConfigurationType.ServiceProperty) { @@ -203,13 +203,13 @@ public virtual bool IsCandidatePrimitiveProperty(PropertyInfo propertyInfo, ICon return null; } - if (propertyInfo.IsCandidateProperty() - && _typeMappingSource.FindMapping(propertyInfo.GetMemberType(), (IModel)model) != null) + if (memberInfo.IsCandidateProperty() + && _typeMappingSource.FindMapping(memberInfo.GetMemberType(), (IModel)model) != null) { return null; } } - return _parameterBindingFactories.FindFactory(type, propertyInfo.GetSimpleMemberName()); + return _parameterBindingFactories.FindFactory(type, memberInfo.GetSimpleMemberName()); } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index bbedd200447..4f603db2b2e 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -257,11 +257,11 @@ public static string CannotCreateValueGenerator(object? generatorType, object? m generatorType, method); /// - /// The navigation '{1_entityType}.{0_navigation}' cannot be loaded because the entity is not being tracked. Navigations can only be loaded for tracked entities. + /// The navigation '{1_entityType}.{0_navigation}' cannot be loaded because one or more of the key or foreign key properties are shadow properties and the entity is not being tracked. Relationships using shadow values can only be loaded for tracked entities. /// - public static string CannotLoadDetached(object? navigation, object? entityType) + public static string CannotLoadDetachedShadow(object? navigation, object? entityType) => string.Format( - GetString("CannotLoadDetached", "0_navigation", "1_entityType"), + GetString("CannotLoadDetachedShadow", "0_navigation", "1_entityType"), navigation, entityType); /// @@ -3600,7 +3600,7 @@ public static EventDefinition LogInvalidIncludePath(IDiagnostics } /// - /// An attempt was made to lazy-load navigation '{entityType}.{navigation}' after the associated DbContext was disposed. + /// An attempt was made to lazy-load navigation '{entityType}.{navigation}' after the associated DbContext was disposed or returned to the pool, or the entity was explicitly detached from the context. /// public static EventDefinition LogLazyLoadOnDisposedContext(IDiagnosticsLogger logger) { diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 1c041ce9dd9..92cacbca108 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -204,8 +204,8 @@ Cannot create an instance of value generator type '{generatorType}'. Ensure that the type can be instantiated and has a parameterless constructor, or use the overload of '{method}' that accepts a delegate. - - The navigation '{1_entityType}.{0_navigation}' cannot be loaded because the entity is not being tracked. Navigations can only be loaded for tracked entities. + + The navigation '{1_entityType}.{0_navigation}' cannot be loaded because one or more of the key or foreign key properties are shadow properties and the entity is not being tracked. Relationships using shadow values can only be loaded for tracked entities. The type '{type}' cannot be marked as a non-shared type since a shared type entity type with this CLR type exists in the model. @@ -810,7 +810,7 @@ Error CoreEventId.InvalidIncludePathError object object - An attempt was made to lazy-load navigation '{entityType}.{navigation}' after the associated DbContext was disposed. + An attempt was made to lazy-load navigation '{entityType}.{navigation}' after the associated DbContext was disposed or returned to the pool, or the entity was explicitly detached from the context. Warning CoreEventId.LazyLoadOnDisposedContextWarning string string diff --git a/src/Shared/PropertyInfoExtensions.cs b/src/Shared/PropertyInfoExtensions.cs index e1202f20ef4..32dc9037308 100644 --- a/src/Shared/PropertyInfoExtensions.cs +++ b/src/Shared/PropertyInfoExtensions.cs @@ -15,13 +15,16 @@ internal static class PropertyInfoExtensions public static bool IsStatic(this PropertyInfo property) => (property.GetMethod ?? property.SetMethod)!.IsStatic; - public static bool IsCandidateProperty(this PropertyInfo propertyInfo, bool needsWrite = true, bool publicOnly = true) - => !propertyInfo.IsStatic() + public static bool IsCandidateProperty(this MemberInfo memberInfo, bool needsWrite = true, bool publicOnly = true) + => memberInfo is PropertyInfo propertyInfo + ? !propertyInfo.IsStatic() && propertyInfo.CanRead && (!needsWrite || propertyInfo.FindSetterProperty() != null) && propertyInfo.GetMethod != null && (!publicOnly || propertyInfo.GetMethod.IsPublic) - && propertyInfo.GetIndexParameters().Length == 0; + && propertyInfo.GetIndexParameters().Length == 0 + : memberInfo is FieldInfo { IsStatic: false } fieldInfo + && (!publicOnly || fieldInfo.IsPublic); public static bool IsIndexerProperty(this PropertyInfo propertyInfo) { diff --git a/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs index 4acaadd7cca..9b67bc6cd95 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs @@ -207,6 +207,8 @@ public void Lazy_loading_is_logged_only_when_actually_loading() .GenerateMessage("WarningAsErrorEntity", "Nav"), loggerFactory.Log.Select(l => l.Message)); + var entityEntry = context.Entry(entity); + var loaded = entityEntry.Navigation("Nav").IsLoaded; loggerFactory.Clear(); Assert.NotNull(entity.Nav); Assert.DoesNotContain( diff --git a/test/EFCore.Specification.Tests/F1FixtureBase.cs b/test/EFCore.Specification.Tests/F1FixtureBase.cs index 8532c43715b..95fa9edb64a 100644 --- a/test/EFCore.Specification.Tests/F1FixtureBase.cs +++ b/test/EFCore.Specification.Tests/F1FixtureBase.cs @@ -234,10 +234,7 @@ private static void ConfigureConstructorBinding( if (loaderField != null) { - var loaderProperty = typeof(TLoaderEntity) == typeof(TEntity) - ? entityType.AddServiceProperty(loaderField!, ConfigurationSource.Explicit) - : entityType.FindServiceProperty(loaderField.Name)!; - + var loaderProperty = entityType.FindServiceProperty(loaderField.Name)!; parameterBindings.Add(new DependencyInjectionParameterBinding(typeof(ILazyLoader), typeof(ILazyLoader), loaderProperty)); } diff --git a/test/EFCore.Specification.Tests/FieldsOnlyLoadTestBase.cs b/test/EFCore.Specification.Tests/FieldsOnlyLoadTestBase.cs index de7fbf3fac1..8d717ab3216 100644 --- a/test/EFCore.Specification.Tests/FieldsOnlyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/FieldsOnlyLoadTestBase.cs @@ -3517,8 +3517,8 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_sha public virtual async Task Load_many_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildShadowFk { Id = 767 }).Entity; + var child = context.Attach(new ChildShadowFk { Id = 767 }).Entity; + context.Entry(child).Property("ParentId").CurrentValue = null; ClearLog(); @@ -3555,8 +3555,8 @@ public virtual async Task Load_many_to_one_reference_to_principal_null_FK_shadow public virtual async Task Load_one_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleShadowFk { Id = 767 }).Entity; + var single = context.Attach(new SingleShadowFk { Id = 767 }).Entity; + context.Entry(single).Property("ParentId").CurrentValue = null; ClearLog(); @@ -3594,8 +3594,8 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK_shadow_ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildShadowFk { Id = 767 }).Entity; + var child = context.Attach(new ChildShadowFk { Id = 767 }).Entity; + context.Entry(child).Property("ParentId").CurrentValue = null; ClearLog(); @@ -3629,8 +3629,8 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_nu public virtual async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleShadowFk { Id = 767 }).Entity; + var single = context.Attach(new SingleShadowFk { Id = 767 }).Entity; + context.Entry(single).Property("ParentId").CurrentValue = null; ClearLog(); @@ -4224,20 +4224,14 @@ public virtual async Task Load_collection_for_detached_throws(bool async, bool n context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } [ConditionalTheory] @@ -4257,20 +4251,14 @@ public virtual async Task Load_collection_using_string_for_detached_throws(bool context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } [ConditionalTheory] @@ -4290,20 +4278,14 @@ public virtual async Task Load_collection_with_navigation_for_detached_throws(bo context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } [ConditionalTheory] @@ -4323,20 +4305,14 @@ public virtual async Task Load_reference_to_principal_for_detached_throws(bool a context.Entry(child).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } } [ConditionalTheory] @@ -4356,20 +4332,14 @@ public virtual async Task Load_reference_with_navigation_to_principal_for_detach context.Entry(child).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } } [ConditionalTheory] @@ -4389,20 +4359,14 @@ public virtual async Task Load_reference_using_string_to_principal_for_detached_ context.Entry(child).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } } [ConditionalTheory] @@ -4422,20 +4386,14 @@ public virtual async Task Load_reference_to_dependent_for_detached_throws(bool a context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } } [ConditionalTheory] @@ -4455,20 +4413,14 @@ public virtual async Task Load_reference_to_dependent_with_navigation_for_detach context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } } [ConditionalTheory] @@ -4488,20 +4440,14 @@ public virtual async Task Load_reference_to_dependent_using_string_for_detached_ context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } } [ConditionalTheory] @@ -4519,9 +4465,7 @@ public virtual void Query_collection_for_detached_throws(bool noTracking) context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - Assert.Throws(() => collectionEntry.Query()).Message); + var query = collectionEntry.Query(); } [ConditionalTheory] @@ -4539,9 +4483,7 @@ public virtual void Query_collection_using_string_for_detached_throws(bool noTra context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - Assert.Throws(() => collectionEntry.Query()).Message); + var query = collectionEntry.Query(); } [ConditionalTheory] @@ -4559,9 +4501,7 @@ public virtual void Query_collection_with_navigation_for_detached_throws(bool no context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - Assert.Throws(() => collectionEntry.Query()).Message); + var query = collectionEntry.Query(); } [ConditionalTheory] @@ -4579,9 +4519,7 @@ public virtual void Query_reference_to_principal_for_detached_throws(bool noTrac context.Entry(child).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - Assert.Throws(() => referenceEntry.Query()).Message); + var query = referenceEntry.Query(); } [ConditionalTheory] @@ -4599,9 +4537,7 @@ public virtual void Query_reference_with_navigation_to_principal_for_detached_th context.Entry(child).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - Assert.Throws(() => referenceEntry.Query()).Message); + var query = referenceEntry.Query(); } [ConditionalTheory] @@ -4619,9 +4555,7 @@ public virtual void Query_reference_using_string_to_principal_for_detached_throw context.Entry(child).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - Assert.Throws(() => referenceEntry.Query()).Message); + var query = referenceEntry.Query(); } [ConditionalTheory] @@ -4639,9 +4573,7 @@ public virtual void Query_reference_to_dependent_for_detached_throws(bool noTrac context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - Assert.Throws(() => referenceEntry.Query()).Message); + var query = referenceEntry.Query(); } [ConditionalTheory] @@ -4659,9 +4591,7 @@ public virtual void Query_reference_to_dependent_with_navigation_for_detached_th context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - Assert.Throws(() => referenceEntry.Query()).Message); + var query = referenceEntry.Query(); } [ConditionalTheory] @@ -4679,9 +4609,7 @@ public virtual void Query_reference_to_dependent_using_string_for_detached_throw context.Entry(parent).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - Assert.Throws(() => referenceEntry.Query()).Message); + var query = referenceEntry.Query(); } protected class Parent diff --git a/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs b/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs index b24ac9e6d42..76e81817fc5 100644 --- a/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs +++ b/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs @@ -52,19 +52,24 @@ public virtual void Detected_dependent_reference_navigation_changes_are_detected [ConditionalTheory] // Issue #13138 [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_with_recursive_property(EntityState state) { - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { var child = context.Set().Single(); var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } - Assert.True(referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); Assert.NotNull(child.Parent); @@ -72,14 +77,17 @@ public virtual void Lazy_load_one_to_one_reference_with_recursive_property(Entit context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Equal(parent.Id, child.IdLoadedFromParent); + Assert.Equal(parent.Id, child.IdLoadedFromParent); - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.WithRecursiveProperty); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.WithRecursiveProperty); + } } } @@ -262,21 +270,27 @@ public virtual void Attached_collections_are_not_marked_as_loaded(EntityState st [ConditionalTheory] [InlineData(EntityState.Unchanged, false, false)] + [InlineData(EntityState.Added, false, false)] [InlineData(EntityState.Modified, false, false)] [InlineData(EntityState.Deleted, false, false)] + [InlineData(EntityState.Detached, false, false)] [InlineData(EntityState.Unchanged, true, false)] + [InlineData(EntityState.Added, true, false)] [InlineData(EntityState.Modified, true, false)] [InlineData(EntityState.Deleted, true, false)] + [InlineData(EntityState.Detached, true, false)] [InlineData(EntityState.Unchanged, true, true)] + [InlineData(EntityState.Added, true, true)] [InlineData(EntityState.Modified, true, true)] [InlineData(EntityState.Deleted, true, true)] + [InlineData(EntityState.Detached, true, true)] public virtual void Lazy_load_collection(EntityState state, bool useAttach, bool useDetach) { Parent parent = null; if (useAttach) { - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { parent = context.Set().Single(); @@ -286,24 +300,17 @@ public virtual void Lazy_load_collection(EntityState state, bool useAttach, bool } } - if (useDetach) + if (state != EntityState.Detached && useDetach) { Assert.Null(parent.Children); } else { - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.LazyLoadOnDisposedContextWarning.ToString(), - CoreResources.LogLazyLoadOnDisposedContext(new TestLogger()) - .GenerateMessage("MotherProxy", "Children"), - "CoreEventId.LazyLoadOnDisposedContextWarning"), - Assert.Throws( - () => parent.Children).Message); + AssertDisposed(() => parent.Children, "MotherProxy", "Children"); } } - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -313,45 +320,61 @@ public virtual void Lazy_load_collection(EntityState state, bool useAttach, bool var collectionEntry = context.Entry(parent).Collection(e => e.Children); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(collectionEntry.IsLoaded); changeDetector.DetectChangesCalled = false; - Assert.NotNull(parent.Children); + if (state == EntityState.Detached && useAttach) + { + AssertDisposed(() => parent.Children, "MotherProxy", "Children"); + } + else + { + Assert.NotNull(parent.Children); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(collectionEntry.IsLoaded); + //Assert.True(collectionEntry.IsLoaded); - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, parent.Children.Count()); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, parent.Children.Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, false, false)] + [InlineData(EntityState.Added, false, false)] [InlineData(EntityState.Modified, false, false)] [InlineData(EntityState.Deleted, false, false)] + [InlineData(EntityState.Detached, false, false)] [InlineData(EntityState.Unchanged, true, false)] + [InlineData(EntityState.Added, true, false)] [InlineData(EntityState.Modified, true, false)] [InlineData(EntityState.Deleted, true, false)] + [InlineData(EntityState.Detached, true, false)] [InlineData(EntityState.Unchanged, true, true)] + [InlineData(EntityState.Added, true, true)] [InlineData(EntityState.Modified, true, true)] [InlineData(EntityState.Deleted, true, true)] + [InlineData(EntityState.Detached, true, true)] public virtual void Lazy_load_many_to_one_reference_to_principal(EntityState state, bool useAttach, bool useDetach) { Child child = null; if (useAttach) { - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { child = context.Set().Single(e => e.Id == 12); @@ -361,24 +384,17 @@ public virtual void Lazy_load_many_to_one_reference_to_principal(EntityState sta } } - if (useDetach) + if (state != EntityState.Detached && useDetach) { Assert.Null(child.Parent); } else { - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.LazyLoadOnDisposedContextWarning.ToString(), - CoreResources.LogLazyLoadOnDisposedContext(new TestLogger()) - .GenerateMessage("ChildProxy", "Parent"), - "CoreEventId.LazyLoadOnDisposedContextWarning"), - Assert.Throws( - () => child.Parent).Message); + AssertDisposed(() => child.Parent, "ChildProxy", "Parent"); } } - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -388,62 +404,86 @@ public virtual void Lazy_load_many_to_one_reference_to_principal(EntityState sta var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); changeDetector.DetectChangesCalled = false; - if (state == EntityState.Deleted) + if (state == EntityState.Detached && useAttach) { - Assert.Null(child.Parent); + AssertDisposed(() => child.Parent, "ChildProxy", "Parent"); } else { - Assert.NotNull(child.Parent); - } + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Deleted) + { + Assert.Contains(child, child.Parent!.Children); + } - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.Children); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, false, false)] + [InlineData(EntityState.Added, false, false)] [InlineData(EntityState.Modified, false, false)] [InlineData(EntityState.Deleted, false, false)] + [InlineData(EntityState.Detached, false, false)] [InlineData(EntityState.Unchanged, true, false)] + [InlineData(EntityState.Added, true, false)] [InlineData(EntityState.Modified, true, false)] [InlineData(EntityState.Deleted, true, false)] + [InlineData(EntityState.Detached, true, false)] [InlineData(EntityState.Unchanged, true, true)] + [InlineData(EntityState.Added, true, true)] [InlineData(EntityState.Modified, true, true)] [InlineData(EntityState.Deleted, true, true)] + [InlineData(EntityState.Detached, true, true)] public virtual void Lazy_load_one_to_one_reference_to_principal(EntityState state, bool useAttach, bool useDetach) { Single single = null; if (useAttach) { - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { single = context.Set().Single(); @@ -453,24 +493,17 @@ public virtual void Lazy_load_one_to_one_reference_to_principal(EntityState stat } } - if (useDetach) + if (state != EntityState.Detached && useDetach) { Assert.Null(single.Parent); } else { - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.LazyLoadOnDisposedContextWarning.ToString(), - CoreResources.LogLazyLoadOnDisposedContext(new TestLogger()) - .GenerateMessage("SingleProxy", "Parent"), - "CoreEventId.LazyLoadOnDisposedContextWarning"), - Assert.Throws( - () => single.Parent).Message); + AssertDisposed(() => single.Parent, "SingleProxy", "Parent"); } } - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -480,62 +513,86 @@ public virtual void Lazy_load_one_to_one_reference_to_principal(EntityState stat var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); changeDetector.DetectChangesCalled = false; - if (state == EntityState.Deleted) + if (state == EntityState.Detached && useAttach) { - Assert.Null(single.Parent); + AssertDisposed(() => single.Parent, "SingleProxy", "Parent"); } else { - Assert.NotNull(single.Parent); - } + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.Single); + } - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.Single); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, false, false)] + [InlineData(EntityState.Added, false, false)] [InlineData(EntityState.Modified, false, false)] [InlineData(EntityState.Deleted, false, false)] + [InlineData(EntityState.Detached, false, false)] [InlineData(EntityState.Unchanged, true, false)] + [InlineData(EntityState.Added, true, false)] [InlineData(EntityState.Modified, true, false)] [InlineData(EntityState.Deleted, true, false)] + [InlineData(EntityState.Detached, true, false)] [InlineData(EntityState.Unchanged, true, true)] + [InlineData(EntityState.Added, true, true)] [InlineData(EntityState.Modified, true, true)] [InlineData(EntityState.Deleted, true, true)] + [InlineData(EntityState.Detached, true, true)] public virtual void Lazy_load_one_to_one_reference_to_dependent(EntityState state, bool useAttach, bool useDetach) { Parent parent = null; if (useAttach) { - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { parent = context.Set().Single(); @@ -545,24 +602,17 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent(EntityState stat } } - if (useDetach) + if (state != EntityState.Detached && useDetach) { Assert.Null(parent.Single); } else { - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.LazyLoadOnDisposedContextWarning.ToString(), - CoreResources.LogLazyLoadOnDisposedContext(new TestLogger()) - .GenerateMessage("MotherProxy", "Single"), - "CoreEventId.LazyLoadOnDisposedContextWarning"), - Assert.Throws( - () => parent.Single).Message); + AssertDisposed(() => parent.Single, "MotherProxy", "Single"); } } - using (var context = CreateContext(lazyLoadingEnabled: true)) + using (var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached)) { var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -572,37 +622,53 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent(EntityState stat var referenceEntry = context.Entry(parent).Reference(e => e.Single); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(referenceEntry.IsLoaded); changeDetector.DetectChangesCalled = false; - Assert.NotNull(parent.Single); + if (state == EntityState.Detached && useAttach) + { + AssertDisposed(() => parent.Single, "MotherProxy", "Single"); + } + else + { + Assert.NotNull(parent.Single); + Assert.Same(parent, parent.Single.Parent); - Assert.False(changeDetector.DetectChangesCalled); + Assert.False(changeDetector.DetectChangesCalled); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var single = context.Set().Single(); @@ -611,7 +677,10 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityS var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -633,29 +702,39 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityS RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.SinglePkToPk); + Assert.Same(single, single.Parent!.SinglePkToPk); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SinglePkToPk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var parent = context.Set().Single(); @@ -664,7 +743,10 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityS var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -679,12 +761,20 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityS RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.SinglePkToPk.Parent); + } - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } } [ConditionalFact] @@ -803,11 +893,13 @@ public virtual void Setting_reference_to_owned_type_to_null_is_allowed_on_virtua [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var child = context.CreateProxy(); @@ -819,7 +911,10 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK(EntityS var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -840,11 +935,13 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK(EntityS [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var single = context.CreateProxy(); @@ -856,7 +953,10 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK(EntitySt var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -878,24 +978,32 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK(EntitySt [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_changed_non_found_FK(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var child = context.CreateProxy(); child.Id = 767; child.ParentId = 797; - context.Attach(child); + if (state != EntityState.Detached) + { + context.Attach(child); + } ClearLog(); var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } referenceEntry.IsLoaded = true; @@ -903,7 +1011,14 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_changed_non_fou child.ParentId = 707; - context.ChangeTracker.DetectChanges(); + if (state != EntityState.Detached) + { + context.ChangeTracker.DetectChanges(); + } + else + { + referenceEntry.IsLoaded = false; + } if (state == EntityState.Deleted) { @@ -914,36 +1029,49 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_changed_non_fou Assert.NotNull(child.Parent); } - Assert.True(changeDetector.DetectChangesCalled); + if (state != EntityState.Detached) + { + Assert.True(changeDetector.DetectChangesCalled); + } Assert.True(referenceEntry.IsLoaded); RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(parent.Children); - Assert.Null(child.Parent); + Assert.Contains(child, child.Parent!.Children); } - else + + if (state != EntityState.Detached) { - Assert.Same(child, parent.Children.Single()); - Assert.Same(parent, child.Parent); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(parent.Children); + Assert.Null(child.Parent); + } + else + { + Assert.Same(child, parent.Children.Single()); + Assert.Same(parent, child.Parent); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_changed_found_FK(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var parent = context.CreateProxy(); @@ -957,14 +1085,20 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_changed_found_F child.Parent = parent; parent.Children = new List { child }; - context.Attach(child); - context.Attach(parent); + if (state != EntityState.Detached) + { + context.Attach(child); + context.Attach(parent); + } ClearLog(); var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } referenceEntry.IsLoaded = true; @@ -990,29 +1124,39 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_changed_found_F RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - - var newParent = context.ChangeTracker.Entries().Single(e => e.Entity.Id != parent.Id).Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Empty(parent.Children); - Assert.Null(child.Parent); + Assert.Contains(child, child.Parent!.Children); } - else + + if (state != EntityState.Detached) { - Assert.Same(child, newParent.Children.Single()); - Assert.Same(newParent, child.Parent); + var newParent = context.ChangeTracker.Entries().Single(e => e.Entity.Id != parent.Id).Entity; + + if (state == EntityState.Deleted) + { + Assert.Empty(parent.Children); + Assert.Null(child.Parent); + } + else + { + Assert.Same(child, newParent.Children.Single()); + Assert.Same(newParent, child.Parent); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_collection_not_found(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var parent = context.CreateProxy(); @@ -1025,7 +1169,10 @@ public virtual void Lazy_load_collection_not_found(EntityState state) var collectionEntry = context.Entry(parent).Collection(e => e.Children); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(collectionEntry.IsLoaded); @@ -1046,11 +1193,13 @@ public virtual void Lazy_load_collection_not_found(EntityState state) [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_not_found(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var child = context.CreateProxy(); @@ -1063,7 +1212,10 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_not_found(Entit var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1084,11 +1236,13 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_not_found(Entit [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_not_found(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var single = context.CreateProxy(); @@ -1101,7 +1255,10 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_not_found(Entity var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1123,11 +1280,13 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_not_found(Entity [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var parent = context.CreateProxy(); @@ -1140,7 +1299,10 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found(Entity var referenceEntry = context.Entry(parent).Reference(e => e.Single); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1162,17 +1324,23 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found(Entity [ConditionalTheory] [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, CascadeTiming.Never)] + [InlineData(EntityState.Added, CascadeTiming.Never)] [InlineData(EntityState.Modified, CascadeTiming.Never)] [InlineData(EntityState.Deleted, CascadeTiming.Never)] + [InlineData(EntityState.Detached, CascadeTiming.Never)] public virtual void Lazy_load_collection_already_loaded(EntityState state, CascadeTiming cascadeDeleteTiming) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); context.ChangeTracker.CascadeDeleteTiming = cascadeDeleteTiming; var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -1183,7 +1351,10 @@ public virtual void Lazy_load_collection_already_loaded(EntityState state, Casca var collectionEntry = context.Entry(parent).Collection(e => e.Children); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.True(collectionEntry.IsLoaded); @@ -1210,24 +1381,27 @@ public virtual void Lazy_load_collection_already_loaded(EntityState state, Casca Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); } - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, CascadeTiming.Never)] + [InlineData(EntityState.Added, CascadeTiming.Never)] [InlineData(EntityState.Modified, CascadeTiming.Never)] [InlineData(EntityState.Deleted, CascadeTiming.Never)] public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded( EntityState state, CascadeTiming cascadeDeleteTiming) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); context.ChangeTracker.CascadeDeleteTiming = cascadeDeleteTiming; var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -1238,7 +1412,10 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded( var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.True(referenceEntry.IsLoaded); @@ -1253,21 +1430,31 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded( RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Deleted) + { + Assert.Contains(child, child.Parent!.Children); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var single = context.Set().Include(e => e.Parent).Single(); @@ -1276,7 +1463,10 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded(E var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.True(referenceEntry.IsLoaded); @@ -1291,29 +1481,40 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded(E RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent.Single); + } - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, CascadeTiming.Never)] + [InlineData(EntityState.Added, CascadeTiming.Never)] [InlineData(EntityState.Modified, CascadeTiming.Never)] [InlineData(EntityState.Deleted, CascadeTiming.Never)] public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded( EntityState state, CascadeTiming cascadeDeleteTiming) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); context.ChangeTracker.CascadeDeleteTiming = cascadeDeleteTiming; var changeDetector = (ChangeDetectorProxy)context.GetService(); @@ -1324,7 +1525,10 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded( var referenceEntry = context.Entry(parent).Reference(e => e.Single); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.True(referenceEntry.IsLoaded); @@ -1339,31 +1543,41 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded( RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.Single); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (cascadeDeleteTiming == CascadeTiming.Immediate - && state == EntityState.Deleted) + if (state != EntityState.Deleted) { - // No fixup to Deleted entity. - Assert.Null(single.Parent); + Assert.Same(parent, parent.Single.Parent); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + + if (cascadeDeleteTiming == CascadeTiming.Immediate + && state == EntityState.Deleted) + { + // No fixup to Deleted entity. + Assert.Null(single.Parent); + } + else + { + Assert.Same(parent, single.Parent); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var single = context.Set().Include(e => e.Parent).Single(); @@ -1372,7 +1586,10 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.True(referenceEntry.IsLoaded); @@ -1387,21 +1604,31 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent.SinglePkToPk); + } - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var changeDetector = (ChangeDetectorProxy)context.GetService(); var parent = context.Set().Include(e => e.SinglePkToPk).Single(); @@ -1410,7 +1637,10 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.True(referenceEntry.IsLoaded); @@ -1425,28 +1655,41 @@ public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.SinglePkToPk.Parent); + } + + if (state != EntityState.Detached) + { + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_alternate_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var child = context.Set().Single(e => e.Id == 32); ClearLog(); var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1464,36 +1707,49 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_alternate_key(E RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenAk); + Assert.Same(child, child.Parent!.ChildrenAk.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenAk.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenAk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenAk.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_alternate_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var single = context.Set().Single(); ClearLog(); var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1511,36 +1767,49 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_alternate_key(En RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleAk); + Assert.Same(single, single.Parent!.SingleAk); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleAk); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleAk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_dependent_alternate_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var parent = context.Set().Single(); ClearLog(); var referenceEntry = context.Entry(parent).Reference(e => e.SingleAk); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1551,21 +1820,31 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_alternate_key(En RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.SingleAk.Parent); + } - Assert.Same(single, parent.SingleAk); - Assert.Same(parent, single.Parent); + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SingleAk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var child = context.CreateProxy(); child.Id = 767; @@ -1575,7 +1854,10 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_alterna var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1592,11 +1874,13 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_alterna [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var single = context.CreateProxy(); single.Id = 767; @@ -1606,7 +1890,10 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_alternat var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1624,18 +1911,23 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_alternat [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_collection_shadow_fk(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var parent = context.Set().Single(); ClearLog(); var collectionEntry = context.Entry(parent).Collection(e => e.ChildrenShadowFk); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(collectionEntry.IsLoaded); @@ -1649,117 +1941,154 @@ public virtual void Lazy_load_collection_shadow_fk(EntityState state) Assert.Equal(2, parent.ChildrenShadowFk.Count()); Assert.All(parent.ChildrenShadowFk.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_shadow_fk(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var child = context.Set().Single(e => e.Id == 52); ClearLog(); - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) + if (state == EntityState.Detached) { - Assert.Null(child.Parent); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => child.Parent).Message); } else { - Assert.NotNull(child.Parent); - } + var referenceEntry = context.Entry(child).Reference(e => e.Parent); - Assert.True(referenceEntry.IsLoaded); + context.Entry(child).State = state; - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - var parent = context.ChangeTracker.Entries().Single().Entity; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenShadowFk); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenShadowFk.Single()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.ChildrenShadowFk.Single()); + } + + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenShadowFk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenShadowFk.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_shadow_fk(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var single = context.Set().Single(); ClearLog(); - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) + if (state == EntityState.Detached) { - Assert.Null(single.Parent); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + Assert.Throws(() => single.Parent).Message); } else { - Assert.NotNull(single.Parent); - } + var referenceEntry = context.Entry(single).Reference(e => e.Parent); - Assert.True(referenceEntry.IsLoaded); + context.Entry(single).State = state; - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.True(referenceEntry.IsLoaded); - var parent = context.ChangeTracker.Entries().Single().Entity; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleShadowFk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleShadowFk); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.SingleShadowFk); + } + + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleShadowFk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_dependent_shadow_fk(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var parent = context.Set().Single(); ClearLog(); var referenceEntry = context.Entry(parent).Reference(e => e.SingleShadowFk); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1770,31 +2099,45 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_shadow_fk(Entity RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.SingleShadowFk!.Parent); + } - Assert.Same(single, parent.SingleShadowFk); - Assert.Same(parent, single.Parent); + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SingleShadowFk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var child = context.CreateProxy(); child.Id = 767; context.Attach(child); + context.Entry(child).Property("ParentId").CurrentValue = null; ClearLog(); var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1811,21 +2154,27 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_ [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var single = context.CreateProxy(); single.Id = 767; context.Attach(single); + context.Entry(single).Property("ParentId").CurrentValue = null; ClearLog(); var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1843,18 +2192,23 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_f [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_collection_composite_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var parent = context.Set().Single(); ClearLog(); var collectionEntry = context.Entry(parent).Collection(e => e.ChildrenCompositeKey); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(collectionEntry.IsLoaded); @@ -1868,23 +2222,28 @@ public virtual void Lazy_load_collection_composite_key(EntityState state) Assert.Equal(2, parent.ChildrenCompositeKey.Count()); Assert.All(parent.ChildrenCompositeKey.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_composite_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var child = context.Set().Single(e => e.Id == 52); ClearLog(); var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1902,36 +2261,49 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_composite_key(E RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenCompositeKey); + Assert.Same(child, child.Parent!.ChildrenCompositeKey.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenCompositeKey.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenCompositeKey); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenCompositeKey.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_composite_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var single = context.Set().Single(); ClearLog(); var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1949,36 +2321,49 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_composite_key(En RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleCompositeKey); + Assert.Same(single, single.Parent!.SingleCompositeKey); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleCompositeKey); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleCompositeKey); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_dependent_composite_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var parent = context.Set().Single(); ClearLog(); var referenceEntry = context.Entry(parent).Reference(e => e.SingleCompositeKey); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -1989,21 +2374,31 @@ public virtual void Lazy_load_one_to_one_reference_to_dependent_composite_key(En RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.SingleCompositeKey.Parent); + } - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleCompositeKey); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var child = context.CreateProxy(); child.Id = 767; child.ParentId = 567; @@ -2014,7 +2409,10 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_composi var referenceEntry = context.Entry(child).Reference(e => e.Parent); - context.Entry(child).State = state; + if (state != EntityState.Detached) + { + context.Entry(child).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -2031,11 +2429,13 @@ public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_composi [ConditionalTheory] [InlineData(EntityState.Unchanged)] + [InlineData(EntityState.Added)] [InlineData(EntityState.Modified)] [InlineData(EntityState.Deleted)] + [InlineData(EntityState.Detached)] public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key(EntityState state) { - using var context = CreateContext(lazyLoadingEnabled: true); + using var context = CreateContext(lazyLoadingEnabled: true, tracking: state != EntityState.Detached); var single = context.CreateProxy(); single.Id = 767; single.ParentAlternateId = "Boot"; @@ -2046,7 +2446,10 @@ public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_composit var referenceEntry = context.Entry(single).Reference(e => e.Parent); - context.Entry(single).State = state; + if (state != EntityState.Detached) + { + context.Entry(single).State = state; + } Assert.False(referenceEntry.IsLoaded); @@ -2095,54 +2498,6 @@ public virtual void Lazy_load_reference_to_dependent_for_detached_is_no_op() Assert.Null(parent.Single); } - [ConditionalFact] - public virtual void Lazy_load_collection_for_no_tracking_throws() - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().AsNoTracking().Single(); - - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.DetachedLazyLoadingWarning.ToString(), - CoreResources.LogDetachedLazyLoading(new TestLogger()) - .GenerateMessage(nameof(Parent.Children), "MotherProxy"), - "CoreEventId.DetachedLazyLoadingWarning"), - Assert.Throws( - () => parent.Children).Message); - } - - [ConditionalFact] - public virtual void Lazy_load_reference_to_principal_for_no_tracking_throws() - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Set().AsNoTracking().Single(e => e.Id == 12); - - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.DetachedLazyLoadingWarning.ToString(), - CoreResources.LogDetachedLazyLoading(new TestLogger()) - .GenerateMessage(nameof(Child.Parent), "ChildProxy"), - "CoreEventId.DetachedLazyLoadingWarning"), - Assert.Throws( - () => child.Parent).Message); - } - - [ConditionalFact] - public virtual void Lazy_load_reference_to_dependent_for_no_tracking_throws() - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().AsNoTracking().Single(); - - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.DetachedLazyLoadingWarning.ToString(), - CoreResources.LogDetachedLazyLoading(new TestLogger()) - .GenerateMessage(nameof(Parent.Single), "MotherProxy"), - "CoreEventId.DetachedLazyLoadingWarning"), - Assert.Throws( - () => parent.Single).Message); - } - [ConditionalFact] public virtual void Lazy_load_collection_for_no_tracking_does_not_throw_if_populated() { @@ -2189,16 +2544,21 @@ public virtual void Lazy_load_reference_to_dependent_for_no_tracking_does_not_th [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection(EntityState state, bool async) { - using var context = CreateContext(); + using var context = CreateContext(tracking: state != EntityState.Detached); var parent = context.Set().Single(); ClearLog(); var collectionEntry = context.Entry(parent).Collection(e => e.Children); - context.Entry(parent).State = state; + if (state != EntityState.Detached) + { + context.Entry(parent).State = state; + } Assert.False(collectionEntry.IsLoaded); @@ -2219,7 +2579,7 @@ public virtual async Task Load_collection(EntityState state, bool async) Assert.Equal(2, parent.Children.Count()); Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalFact] @@ -2929,10 +3289,23 @@ public class OwnedAddress public int CountryCode { get; set; } } - protected DbContext CreateContext(bool lazyLoadingEnabled = false) + private static void AssertDisposed(Func testCode, string entityTypeName, string navigationName) + => Assert.Equal( + CoreStrings.WarningAsErrorTemplate( + CoreEventId.LazyLoadOnDisposedContextWarning.ToString(), + CoreResources.LogLazyLoadOnDisposedContext(new TestLogger()) + .GenerateMessage(entityTypeName, navigationName), + "CoreEventId.LazyLoadOnDisposedContextWarning"), + Assert.Throws(testCode).Message); + + protected DbContext CreateContext(bool lazyLoadingEnabled = false, bool tracking = true) + => CreateContext(lazyLoadingEnabled, tracking ? QueryTrackingBehavior.TrackAll : QueryTrackingBehavior.NoTracking); + + protected DbContext CreateContext(bool lazyLoadingEnabled, QueryTrackingBehavior trackingBehavior) { var context = Fixture.CreateContext(); context.ChangeTracker.LazyLoadingEnabled = lazyLoadingEnabled; + context.ChangeTracker.QueryTrackingBehavior = trackingBehavior; return context; } diff --git a/test/EFCore.Specification.Tests/LoadTestBase.cs b/test/EFCore.Specification.Tests/LoadTestBase.cs index b4bd6b9df51..0361144ba50 100644 --- a/test/EFCore.Specification.Tests/LoadTestBase.cs +++ b/test/EFCore.Specification.Tests/LoadTestBase.cs @@ -3,12 +3,11 @@ using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; -using Microsoft.EntityFrameworkCore.Diagnostics.Internal; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore; -public abstract class LoadTestBase : IClassFixture +public abstract partial class LoadTestBase : IClassFixture where TFixture : LoadTestBase.LoadFixtureBase { protected LoadTestBase(TFixture fixture) @@ -155,1471 +154,49 @@ public virtual void Attached_collections_are_not_marked_as_loaded(EntityState st Assert.False(context.Entry(parent).Collection(e => e.ChildrenCompositeKey).IsLoaded); } - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_collection(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Set().Single(); - - ClearLog(); - - var collectionEntry = context.Entry(parent).Collection(e => e.Children); - - context.Entry(parent).State = state; - - Assert.False(collectionEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(parent.Children); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(collectionEntry.IsLoaded); - - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, parent.Children.Count()); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var child = context.Set().Single(e => e.Id == 12); - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - } - else - { - Assert.NotNull(child.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.Children); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var single = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - } - else - { - Assert.NotNull(single.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.Single); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_dependent(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.Single); - - context.Entry(parent).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(parent.Single); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var single = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - } - else - { - Assert.NotNull(single.Parent); - } - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SinglePkToPk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); - - context.Entry(parent).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(parent.SinglePkToPk); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var child = context.Attach( - new Child(context.GetService().Load) { Id = 767, ParentId = null }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.Null(child.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - Assert.Null(child.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var single = context.Attach( - new Single(context.GetService().Load) { Id = 767, ParentId = null }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.Null(single.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - - Assert.Null(single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_collection_not_found(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Attach( - new Parent(context.GetService().Load) { Id = 767, AlternateId = "NewRoot" }).Entity; - - ClearLog(); - - var collectionEntry = context.Entry(parent).Collection(e => e.Children); - - context.Entry(parent).State = state; - - Assert.False(collectionEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.Empty(parent.Children); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Empty(parent.Children); - Assert.Single(context.ChangeTracker.Entries()); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_not_found(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var child = context.Attach( - new Child(context.GetService().Load) { Id = 767, ParentId = 787 }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.Null(child.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - Assert.Null(child.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_not_found(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var single = context.Attach( - new Single(context.GetService().Load) { Id = 767, ParentId = 787 }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.Null(single.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - - Assert.Null(single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Attach( - new Parent(context.GetService().Load) { Id = 767, AlternateId = "NewRoot" }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.Single); - - context.Entry(parent).State = state; - - Assert.False(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.Null(parent.Single); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - - Assert.Null(parent.Single); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged, CascadeTiming.Immediate)] - [InlineData(EntityState.Modified, CascadeTiming.Immediate)] - [InlineData(EntityState.Deleted, CascadeTiming.Immediate)] - [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges)] - [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges)] - [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges)] - public virtual void Lazy_load_collection_already_loaded(EntityState state, CascadeTiming deleteOrphansTiming) - { - using var context = CreateContext(lazyLoadingEnabled: true); - context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; - - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Set().Include(e => e.Children).Single(); - - ClearLog(); - - var collectionEntry = context.Entry(parent).Collection(e => e.Children); - - context.Entry(parent).State = state; - - Assert.True(collectionEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(parent.Children); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, parent.Children.Count()); - - if (state == EntityState.Deleted - && deleteOrphansTiming != CascadeTiming.Never) - { - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); - } - else - { - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - } - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var child = context.Set().Include(e => e.Parent).Single(e => e.Id == 12); - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.True(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(child.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var single = context.Set().Include(e => e.Parent).Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.True(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(single.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged, CascadeTiming.Immediate)] - [InlineData(EntityState.Modified, CascadeTiming.Immediate)] - [InlineData(EntityState.Deleted, CascadeTiming.Immediate)] - [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges)] - [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges)] - [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges)] - public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded(EntityState state, CascadeTiming deleteOrphansTiming) - { - using var context = CreateContext(lazyLoadingEnabled: true); - context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; - - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Set().Include(e => e.Single).Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.Single); - - context.Entry(parent).State = state; - - Assert.True(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(parent.Single); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.Single); - - if (state == EntityState.Deleted - && deleteOrphansTiming != CascadeTiming.Never) - { - Assert.Null(single.Parent); - } - else - { - Assert.Same(parent, single.Parent); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var single = context.Set().Include(e => e.Parent).Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.True(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(single.Parent); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var changeDetector = (ChangeDetectorProxy)context.GetService(); - - var parent = context.Set().Include(e => e.SinglePkToPk).Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); - - context.Entry(parent).State = state; - - Assert.True(referenceEntry.IsLoaded); - - changeDetector.DetectChangesCalled = false; - - Assert.NotNull(parent.SinglePkToPk); - - Assert.False(changeDetector.DetectChangesCalled); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_alternate_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Set().Single(e => e.Id == 32); - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - } - else - { - Assert.NotNull(child.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenAk); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenAk.Single()); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_alternate_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var single = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - } - else - { - Assert.NotNull(single.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleAk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleAk); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_dependent_alternate_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.SingleAk); - - context.Entry(parent).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.NotNull(parent.SingleAk); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.SingleAk); - Assert.Same(parent, single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Attach( - new ChildAk(context.GetService().Load) { Id = 767, ParentId = null }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.Null(child.Parent); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - Assert.Null(child.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var single = context.Attach( - new SingleAk(context.GetService().Load) { Id = 767, ParentId = null }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.Null(single.Parent); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - - Assert.Null(single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_collection_shadow_fk(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().Single(); - - ClearLog(); - - var collectionEntry = context.Entry(parent).Collection(e => e.ChildrenShadowFk); - - context.Entry(parent).State = state; - - Assert.False(collectionEntry.IsLoaded); - - Assert.NotNull(parent.ChildrenShadowFk); - - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, parent.ChildrenShadowFk.Count()); - Assert.All(parent.ChildrenShadowFk.Select(e => e.Parent), c => Assert.Same(parent, c)); - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_shadow_fk(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Set().Single(e => e.Id == 52); - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - } - else - { - Assert.NotNull(child.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenShadowFk); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenShadowFk.Single()); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_shadow_fk(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var single = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - } - else - { - Assert.NotNull(single.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleShadowFk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleShadowFk); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_dependent_shadow_fk(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.SingleShadowFk); - - context.Entry(parent).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.NotNull(parent.SingleShadowFk); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.SingleShadowFk); - Assert.Same(parent, single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Attach( - new ChildShadowFk(context.GetService().Load) { Id = 767 }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.Null(child.Parent); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - Assert.Null(child.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var single = context.Attach( - new SingleShadowFk(context.GetService().Load) { Id = 767 }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.Null(single.Parent); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - - Assert.Null(single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_collection_composite_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().Single(); - - ClearLog(); - - var collectionEntry = context.Entry(parent).Collection(e => e.ChildrenCompositeKey); - - context.Entry(parent).State = state; - - Assert.False(collectionEntry.IsLoaded); - - Assert.NotNull(parent.ChildrenCompositeKey); - - Assert.True(collectionEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, parent.ChildrenCompositeKey.Count()); - Assert.All(parent.ChildrenCompositeKey.Select(e => e.Parent), c => Assert.Same(parent, c)); - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_composite_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Set().Single(e => e.Id == 52); - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - } - else - { - Assert.NotNull(child.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenCompositeKey); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenCompositeKey.Single()); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_composite_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var single = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - } - else - { - Assert.NotNull(single.Parent); - } - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleCompositeKey); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleCompositeKey); - } - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_dependent_composite_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var parent = context.Set().Single(); - - ClearLog(); - - var referenceEntry = context.Entry(parent).Reference(e => e.SingleCompositeKey); - - context.Entry(parent).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.NotNull(parent.SingleCompositeKey); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.SingleCompositeKey); - Assert.Same(parent, single.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var child = context.Attach( - new ChildCompositeKey(context.GetService().Load) { Id = 767, ParentId = 567 }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - context.Entry(child).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.Null(child.Parent); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - Assert.Null(child.Parent); - } - - [ConditionalTheory] - [InlineData(EntityState.Unchanged)] - [InlineData(EntityState.Modified)] - [InlineData(EntityState.Deleted)] - public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key(EntityState state) - { - using var context = CreateContext(lazyLoadingEnabled: true); - var single = context.Attach( - new SingleCompositeKey(context.GetService().Load) { Id = 767, ParentAlternateId = "Boot" }).Entity; - - ClearLog(); - - var referenceEntry = context.Entry(single).Reference(e => e.Parent); - - context.Entry(single).State = state; - - Assert.False(referenceEntry.IsLoaded); - - Assert.Null(single.Parent); - - Assert.True(referenceEntry.IsLoaded); - - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; - - Assert.Single(context.ChangeTracker.Entries()); - - Assert.Null(single.Parent); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Lazy_load_collection_for_detached_throws(bool noTracking) - { - using var context = CreateContext(lazyLoadingEnabled: true, noTracking: noTracking); - var parent = context.Set().Single(); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.DetachedLazyLoadingWarning.ToString(), - CoreResources.LogDetachedLazyLoading(new TestLogger()) - .GenerateMessage(nameof(Parent.Children), "Parent"), - "CoreEventId.DetachedLazyLoadingWarning"), - Assert.Throws( - () => parent.Children).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Lazy_load_reference_to_principal_for_detached_throws(bool noTracking) - { - using var context = CreateContext(lazyLoadingEnabled: true, noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.DetachedLazyLoadingWarning.ToString(), - CoreResources.LogDetachedLazyLoading(new TestLogger()) - .GenerateMessage(nameof(Child.Parent), "Child"), - "CoreEventId.DetachedLazyLoadingWarning"), - Assert.Throws( - () => child.Parent).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Lazy_load_reference_to_dependent_for_detached_throws(bool noTracking) - { - using var context = CreateContext(lazyLoadingEnabled: true, noTracking: noTracking); - var parent = context.Set().Single(); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.WarningAsErrorTemplate( - CoreEventId.DetachedLazyLoadingWarning.ToString(), - CoreResources.LogDetachedLazyLoading(new TestLogger()) - .GenerateMessage(nameof(Parent.Single), "Parent"), - "CoreEventId.DetachedLazyLoadingWarning"), - Assert.Throws( - () => parent.Single).Message); - } - - [ConditionalFact] - public virtual void Lazy_loading_uses_field_access_when_abstract_base_class_navigation() - { - using var context = CreateContext(lazyLoadingEnabled: true); - var product = context.Set().Single(); - var deposit = product.Deposit; - - Assert.NotNull(deposit); - Assert.Same(deposit, product.Deposit); - } - [ConditionalTheory] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] public virtual async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) { using var context = CreateContext(); - - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; var parent = context.Set().Single(); ClearLog(); - var collectionEntry = context.Entry(parent).Collection(e => e.Children); + SetState(context, parent, state, queryTrackingBehavior); - context.Entry(parent).State = state; + var collectionEntry = context.Entry(parent).Collection(e => e.Children); Assert.False(collectionEntry.IsLoaded); @@ -1640,16 +217,20 @@ public virtual async Task Load_collection(EntityState state, QueryTrackingBehavi Assert.Equal(2, parent.Children.Count()); Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal(EntityState state, bool async) { using var context = CreateContext(); @@ -1676,29 +257,41 @@ public virtual async Task Load_many_to_one_reference_to_principal(EntityState st RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal(EntityState state, bool async) { using var context = CreateContext(); @@ -1725,29 +318,41 @@ public virtual async Task Load_one_to_one_reference_to_principal(EntityState sta RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_when_NoTracking_behavior(EntityState state, bool async) { using var context = CreateContext(); @@ -1776,29 +381,41 @@ public virtual async Task Load_one_to_one_reference_to_principal_when_NoTracking RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(single.Parent); - Assert.Null(parent.Single); + Assert.Same(single, single.Parent.Single); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent(EntityState state, bool async) { using var context = CreateContext(); @@ -1825,21 +442,30 @@ public virtual async Task Load_one_to_one_reference_to_dependent(EntityState sta RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + Assert.Same(parent, parent.Single.Parent); - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal(EntityState state, bool async) { using var context = CreateContext(); @@ -1866,29 +492,36 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal(Entity RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SinglePkToPk); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SinglePkToPk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent(EntityState state, bool async) { using var context = CreateContext(); @@ -1915,21 +548,28 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent(Entity RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query(EntityState state, bool async) { using var context = CreateContext(); @@ -1952,20 +592,33 @@ public virtual async Task Load_collection_using_Query(EntityState state, bool as RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.Children.Count()); - Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.Children)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + if (state == EntityState.Detached) + { + Assert.Empty(parent.Children); + Assert.All(children, c => Assert.Null(c.Parent)); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Equal(2, parent.Children.Count()); + Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.Children)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query(EntityState state, bool async) { using var context = CreateContext(); @@ -1983,33 +636,40 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query(En ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.Children); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); - } + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query(EntityState state, bool async) { using var context = CreateContext(); @@ -2027,33 +687,40 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query(Ent ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.Single); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); - } + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query(EntityState state, bool async) { using var context = CreateContext(); @@ -2071,24 +738,32 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query(Ent ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.NotNull(single); - RecordLog(); + if (state != EntityState.Detached) + { + Assert.True(referenceEntry.IsLoaded); - Assert.NotNull(single); - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_using_Query(EntityState state, bool async) { using var context = CreateContext(); @@ -2106,33 +781,40 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_using_ ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SinglePkToPk); - } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); - } + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + Assert.Null(parent.SinglePkToPk); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using_Query(EntityState state, bool async) { using var context = CreateContext(); @@ -2150,29 +832,36 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using_ ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.NotNull(single); - RecordLog(); + if (state != EntityState.Detached) + { + Assert.True(referenceEntry.IsLoaded); - Assert.NotNull(single); - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_null_FK(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new Child { Id = 767, ParentId = null }).Entity; + var child = context.Attach(new Child { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -2195,22 +884,25 @@ public virtual async Task Load_many_to_one_reference_to_principal_null_FK(Entity RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(child.Parent); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_null_FK(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new Single { Id = 767, ParentId = null }).Entity; + var single = context.Attach(new Single { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -2233,7 +925,7 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK(EntityS RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(single.Parent); } @@ -2241,15 +933,18 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK(EntityS [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_null_FK(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new Child { Id = 767, ParentId = null }).Entity; + var child = context.Attach(new Child { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -2270,21 +965,24 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_nu Assert.Null(parent); Assert.Null(child.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_null_FK(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new Single { Id = 767, ParentId = null }).Entity; + var single = context.Attach(new Single { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -2305,21 +1003,24 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_nul Assert.Null(parent); Assert.Null(single.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_not_found(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -2343,21 +1044,24 @@ public virtual async Task Load_collection_not_found(EntityState state, bool asyn RecordLog(); Assert.Empty(parent.Children); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_not_found(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new Child { Id = 767, ParentId = 787 }).Entity; + var child = context.Attach(new Child { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -2380,22 +1084,25 @@ public virtual async Task Load_many_to_one_reference_to_principal_not_found(Enti RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(child.Parent); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_not_found(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new Single { Id = 767, ParentId = 787 }).Entity; + var single = context.Attach(new Single { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -2418,7 +1125,7 @@ public virtual async Task Load_one_to_one_reference_to_principal_not_found(Entit RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(single.Parent); } @@ -2426,15 +1133,18 @@ public virtual async Task Load_one_to_one_reference_to_principal_not_found(Entit [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_not_found(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -2457,7 +1167,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_not_found(Entit RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(parent.Single); } @@ -2465,15 +1175,18 @@ public virtual async Task Load_one_to_one_reference_to_dependent_not_found(Entit [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_not_found(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -2494,21 +1207,24 @@ public virtual async Task Load_collection_using_Query_not_found(EntityState stat Assert.Empty(children); Assert.Empty(parent.Children); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_not_found(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new Child { Id = 767, ParentId = 787 }).Entity; + var child = context.Attach(new Child { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -2529,21 +1245,24 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_no Assert.Null(parent); Assert.Null(child.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_not_found(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new Single { Id = 767, ParentId = 787 }).Entity; + var single = context.Attach(new Single { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -2564,21 +1283,24 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_not Assert.Null(parent); Assert.Null(single.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_not_found(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -2599,7 +1321,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_not Assert.Null(single); Assert.Null(parent.Single); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] @@ -2609,12 +1331,16 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_not [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_collection_already_loaded(EntityState state, bool async, CascadeTiming deleteOrphansTiming) { using var context = CreateContext(); @@ -2626,6 +1352,11 @@ public virtual async Task Load_collection_already_loaded(EntityState state, bool var collectionEntry = context.Entry(parent).Collection(e => e.Children); + foreach (var child in parent.Children) + { + context.Entry(child).State = state; + } + context.Entry(parent).State = state; Assert.True(collectionEntry.IsLoaded); @@ -2644,27 +1375,22 @@ public virtual async Task Load_collection_already_loaded(EntityState state, bool RecordLog(); Assert.Equal(2, parent.Children.Count()); + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - if (state == EntityState.Deleted - && deleteOrphansTiming != CascadeTiming.Never) - { - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); - } - else - { - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - } - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -2674,9 +1400,10 @@ public virtual async Task Load_many_to_one_reference_to_principal_already_loaded var referenceEntry = context.Entry(child).Reference(e => e.Parent); + context.Entry(child.Parent).State = state; context.Entry(child).State = state; - Assert.True(referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); if (async) { @@ -2691,12 +1418,15 @@ public virtual async Task Load_many_to_one_reference_to_principal_already_loaded RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached && state != EntityState.Deleted) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } } [ConditionalTheory] @@ -2706,12 +1436,16 @@ public virtual async Task Load_many_to_one_reference_to_principal_already_loaded [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_one_to_one_reference_to_principal_already_loaded( EntityState state, bool async, @@ -2726,9 +1460,10 @@ public virtual async Task Load_one_to_one_reference_to_principal_already_loaded( var referenceEntry = context.Entry(single).Reference(e => e.Parent); + context.Entry(single.Parent).State = state; context.Entry(single).State = state; - Assert.True(referenceEntry.IsLoaded); + Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); if (async) { @@ -2743,12 +1478,15 @@ public virtual async Task Load_one_to_one_reference_to_principal_already_loaded( RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached && state != EntityState.Deleted) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } } [ConditionalTheory] @@ -2758,12 +1496,16 @@ public virtual async Task Load_one_to_one_reference_to_principal_already_loaded( [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded( EntityState state, bool async, @@ -2778,6 +1520,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded( var referenceEntry = context.Entry(parent).Reference(e => e.Single); + context.Entry(parent.Single).State = state; context.Entry(parent).State = state; Assert.True(referenceEntry.IsLoaded); @@ -2795,19 +1538,13 @@ public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded( RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.Single); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted - && deleteOrphansTiming != CascadeTiming.Never) - { - Assert.Null(single.Parent); - } - else + if (state != EntityState.Detached) { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); Assert.Same(parent, single.Parent); } } @@ -2815,10 +1552,14 @@ public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded( [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -2828,6 +1569,7 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_alread var referenceEntry = context.Entry(single).Reference(e => e.Parent); + context.Entry(single.Parent).State = state; context.Entry(single).State = state; Assert.True(referenceEntry.IsLoaded); @@ -2845,21 +1587,28 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_alread RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -2869,6 +1618,7 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_alread var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); + context.Entry(parent.SinglePkToPk).State = state; context.Entry(parent).State = state; Assert.True(referenceEntry.IsLoaded); @@ -2886,12 +1636,15 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_alread RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] @@ -2901,12 +1654,16 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_alread [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_collection_using_Query_already_loaded( EntityState state, bool async, @@ -2922,6 +1679,11 @@ public virtual async Task Load_collection_using_Query_already_loaded( var collectionEntry = context.Entry(parent).Collection(e => e.Children); + foreach (var child in parent.Children) + { + context.Entry(child).State = state; + } + context.Entry(parent).State = state; Assert.True(collectionEntry.IsLoaded); @@ -2935,19 +1697,33 @@ public virtual async Task Load_collection_using_Query_already_loaded( RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.Children.Count()); - Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.Children)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Detached) + { + Assert.Equal(2, parent.Children.Count()); + Assert.All(children, c => Assert.Null(c.Parent)); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Equal(2, parent.Children.Count()); + Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.Children)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -2957,32 +1733,44 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_al var referenceEntry = context.Entry(child).Reference(e => e.Parent); + context.Entry(child.Parent).State = state; context.Entry(child).State = state; - Assert.True(referenceEntry.IsLoaded); + if (state != EntityState.Deleted) // FK is null + { + Assert.True(referenceEntry.IsLoaded); - var parent = async - ? await referenceEntry.Query().SingleAsync() - : referenceEntry.Query().Single(); + var parent = async + ? await referenceEntry.Query().SingleAsync() + : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.NotNull(parent); - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + Assert.NotNull(parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -2992,23 +1780,31 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_alr var referenceEntry = context.Entry(single).Reference(e => e.Parent); + context.Entry(single.Parent).State = state; context.Entry(single).State = state; - Assert.True(referenceEntry.IsLoaded); + if (state != EntityState.Deleted) // FK is null + { + Assert.True(referenceEntry.IsLoaded); - var parent = async - ? await referenceEntry.Query().SingleAsync() - : referenceEntry.Query().Single(); + var parent = async + ? await referenceEntry.Query().SingleAsync() + : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.NotNull(parent); - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + Assert.NotNull(parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] @@ -3018,12 +1814,16 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_alr [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_already_loaded( EntityState state, bool async, @@ -3039,6 +1839,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_alr var referenceEntry = context.Entry(parent).Reference(e => e.Single); + context.Entry(parent.Single).State = state; context.Entry(parent).State = state; Assert.True(referenceEntry.IsLoaded); @@ -3051,19 +1852,28 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_alr RecordLog(); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.NotNull(single); - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Detached) + { + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_using_Query_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -3073,6 +1883,7 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_using_ var referenceEntry = context.Entry(single).Reference(e => e.Parent); + context.Entry(single.Parent).State = state; context.Entry(single).State = state; Assert.True(referenceEntry.IsLoaded); @@ -3085,20 +1896,28 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_principal_using_ RecordLog(); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + Assert.NotNull(parent); - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SinglePkToPk); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using_Query_already_loaded(EntityState state, bool async) { using var context = CreateContext(); @@ -3108,6 +1927,7 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using_ var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); + context.Entry(parent.SinglePkToPk).State = state; context.Entry(parent).State = state; Assert.True(referenceEntry.IsLoaded); @@ -3121,19 +1941,25 @@ public virtual async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using_ RecordLog(); Assert.NotNull(single); - Assert.Same(single, parent.SinglePkToPk); - Assert.Same(parent, single.Parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Detached) + { + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] - [InlineData(EntityState.Deleted, true)] - [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3163,16 +1989,20 @@ public virtual async Task Load_collection_untyped(EntityState state, bool async) Assert.Equal(2, parent.Children.Count()); Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3199,29 +2029,41 @@ public virtual async Task Load_many_to_one_reference_to_principal_untyped(Entity RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Deleted) { - Assert.Null(child.Parent); - Assert.Null(parent.Children); + Assert.Same(child, child.Parent.Children.Single()); } - else + + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3248,29 +2090,36 @@ public virtual async Task Load_one_to_one_reference_to_principal_untyped(EntityS RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.Single); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3297,21 +2146,28 @@ public virtual async Task Load_one_to_one_reference_to_dependent_untyped(EntityS RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.Single); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3335,20 +2191,33 @@ public virtual async Task Load_collection_using_Query_untyped(EntityState state, RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.Children.Count()); - Assert.All(children.Select(e => ((Child)e).Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.Children)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + if (state == EntityState.Detached) + { + Assert.Empty(parent.Children); + Assert.All(children, c => Assert.Null(((Child)c).Parent)); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Equal(2, parent.Children.Count()); + Assert.All(children.Select(e => ((Child)e).Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.Children)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3367,33 +2236,40 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_un ? (await navigationEntry.Query().ToListAsync()).Single() : navigationEntry.Query().ToList().Single(); - Assert.Equal(state != EntityState.Deleted, navigationEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(((Parent)parent).Children); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, ((Parent)parent).Children.Single()); - } + if (state == EntityState.Deleted) + { + Assert.False(navigationEntry.IsLoaded); + Assert.Null(child.Parent); + Assert.Null(((Parent)parent).Children); + } + else + { + Assert.True(navigationEntry.IsLoaded); + Assert.Same(parent, child.Parent); + Assert.Same(child, ((Parent)parent).Children.Single()); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3412,33 +2288,40 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_unt ? (await navigationEntry.Query().ToListAsync()).Single() : navigationEntry.Query().ToList().Single(); - Assert.Equal(state != EntityState.Deleted, navigationEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(((Parent)parent).Single); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, ((Parent)parent).Single); - } + if (state == EntityState.Deleted) + { + Assert.False(navigationEntry.IsLoaded); + Assert.Null(single.Parent); + Assert.Null(((Parent)parent).Single); + } + else + { + Assert.True(navigationEntry.IsLoaded); + Assert.Same(parent, single.Parent); + Assert.Same(single, ((Parent)parent).Single); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3457,29 +2340,36 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_unt ? (await navigationEntry.Query().ToListAsync()).Single() : navigationEntry.Query().ToList().Single(); - Assert.True(navigationEntry.IsLoaded); + Assert.NotNull(single); - RecordLog(); + if (state != EntityState.Detached) + { + Assert.True(navigationEntry.IsLoaded); - Assert.NotNull(single); - Assert.Same(single, parent.Single); - Assert.Same(parent, ((Single)single).Parent); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Same(single, parent.Single); + Assert.Same(parent, ((Single)single).Parent); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -3503,21 +2393,24 @@ public virtual async Task Load_collection_not_found_untyped(EntityState state, b RecordLog(); Assert.Empty(parent.Children); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new Child { Id = 767, ParentId = 787 }).Entity; + var child = context.Attach(new Child { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -3540,22 +2433,25 @@ public virtual async Task Load_many_to_one_reference_to_principal_not_found_unty RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(child.Parent); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new Single { Id = 767, ParentId = 787 }).Entity; + var single = context.Attach(new Single { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -3578,7 +2474,7 @@ public virtual async Task Load_one_to_one_reference_to_principal_not_found_untyp RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(single.Parent); } @@ -3586,15 +2482,18 @@ public virtual async Task Load_one_to_one_reference_to_principal_not_found_untyp [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -3617,7 +2516,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_not_found_untyp RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(parent.Single); } @@ -3625,15 +2524,18 @@ public virtual async Task Load_one_to_one_reference_to_dependent_not_found_untyp [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -3655,21 +2557,24 @@ public virtual async Task Load_collection_using_Query_not_found_untyped(EntitySt Assert.Empty(children); Assert.Empty(parent.Children); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new Child { Id = 767, ParentId = 787 }).Entity; + var child = context.Attach(new Child { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -3691,21 +2596,24 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_no Assert.Null(parent); Assert.Null(child.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new Single { Id = 767, ParentId = 787 }).Entity; + var single = context.Attach(new Single { Id = 767, ParentId = 787 }).Entity; ClearLog(); @@ -3727,21 +2635,24 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_not Assert.Null(parent); Assert.Null(single.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_not_found_untyped(EntityState state, bool async) { using var context = CreateContext(); - var parent = context.Attach( - new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; ClearLog(); @@ -3763,7 +2674,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_not Assert.Null(single); Assert.Null(parent.Single); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] @@ -3773,12 +2684,16 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_not [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_collection_already_loaded_untyped(EntityState state, bool async, CascadeTiming deleteOrphansTiming) { using var context = CreateContext(); @@ -3790,6 +2705,11 @@ public virtual async Task Load_collection_already_loaded_untyped(EntityState sta var navigationEntry = context.Entry(parent).Navigation("Children"); + foreach (var child in parent.Children) + { + context.Entry(child).State = state; + } + context.Entry(parent).State = state; Assert.True(navigationEntry.IsLoaded); @@ -3808,27 +2728,22 @@ public virtual async Task Load_collection_already_loaded_untyped(EntityState sta RecordLog(); Assert.Equal(2, parent.Children.Count()); + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - if (state == EntityState.Deleted - && deleteOrphansTiming != CascadeTiming.Never) - { - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); - } - else - { - Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); - } - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_already_loaded_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3838,9 +2753,10 @@ public virtual async Task Load_many_to_one_reference_to_principal_already_loaded var navigationEntry = context.Entry(child).Navigation("Parent"); + context.Entry(child.Parent).State = state; context.Entry(child).State = state; - Assert.True(navigationEntry.IsLoaded); + Assert.Equal(state != EntityState.Deleted, navigationEntry.IsLoaded); if (async) { @@ -3855,21 +2771,28 @@ public virtual async Task Load_many_to_one_reference_to_principal_already_loaded RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached && state != EntityState.Deleted) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.Children.Single()); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_already_loaded_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -3879,9 +2802,10 @@ public virtual async Task Load_one_to_one_reference_to_principal_already_loaded_ var navigationEntry = context.Entry(single).Navigation("Parent"); + context.Entry(single.Parent).State = state; context.Entry(single).State = state; - Assert.True(navigationEntry.IsLoaded); + Assert.Equal(state != EntityState.Deleted, navigationEntry.IsLoaded); if (async) { @@ -3896,12 +2820,15 @@ public virtual async Task Load_one_to_one_reference_to_principal_already_loaded_ RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + if (state != EntityState.Detached && state != EntityState.Deleted) + { + var parent = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } } [ConditionalTheory] @@ -3911,12 +2838,16 @@ public virtual async Task Load_one_to_one_reference_to_principal_already_loaded_ [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded_untyped( EntityState state, bool async, @@ -3931,6 +2862,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded_ var navigationEntry = context.Entry(parent).Navigation("Single"); + context.Entry(parent.Single).State = state; context.Entry(parent).State = state; Assert.True(navigationEntry.IsLoaded); @@ -3948,19 +2880,13 @@ public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded_ RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var single = context.ChangeTracker.Entries().Single().Entity; - - Assert.Same(single, parent.Single); + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted - && deleteOrphansTiming != CascadeTiming.Never) - { - Assert.Null(single.Parent); - } - else + if (state != EntityState.Detached) { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); Assert.Same(parent, single.Parent); } } @@ -3972,12 +2898,16 @@ public virtual async Task Load_one_to_one_reference_to_dependent_already_loaded_ [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_collection_using_Query_already_loaded_untyped( EntityState state, bool async, @@ -3993,6 +2923,11 @@ public virtual async Task Load_collection_using_Query_already_loaded_untyped( var navigationEntry = context.Entry(parent).Navigation("Children"); + foreach (var child in parent.Children) + { + context.Entry(child).State = state; + } + context.Entry(parent).State = state; Assert.True(navigationEntry.IsLoaded); @@ -4007,19 +2942,33 @@ public virtual async Task Load_collection_using_Query_already_loaded_untyped( RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.Children.Count()); - Assert.All(children.Select(e => ((Child)e).Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.Children)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Detached) + { + Assert.Equal(2, parent.Children.Count()); + Assert.All(children, c => Assert.Null(((Child)c).Parent)); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Equal(2, parent.Children.Count()); + Assert.All(children.Select(e => ((Child)e).Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.Children)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_already_loaded_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -4029,33 +2978,45 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_al var navigationEntry = context.Entry(child).Navigation("Parent"); + context.Entry(child.Parent).State = state; context.Entry(child).State = state; - Assert.True(navigationEntry.IsLoaded); + if (state != EntityState.Deleted) // FK is null + { + Assert.True(navigationEntry.IsLoaded); - // Issue #16429 - var parent = async - ? (await navigationEntry.Query().ToListAsync()).Single() - : navigationEntry.Query().ToList().Single(); + // Issue #16429 + var parent = async + ? (await navigationEntry.Query().ToListAsync()).Single() + : navigationEntry.Query().ToList().Single(); - Assert.True(navigationEntry.IsLoaded); + Assert.True(navigationEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.NotNull(parent); - Assert.Same(parent, child.Parent); - Assert.Same(child, ((Parent)parent).Children.Single()); + Assert.NotNull(parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Same(parent, child.Parent); + Assert.Same(child, ((Parent)parent).Children.Single()); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_already_loaded_untyped(EntityState state, bool async) { using var context = CreateContext(); @@ -4065,24 +3026,32 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_alr var navigationEntry = context.Entry(single).Navigation("Parent"); + context.Entry(single.Parent).State = state; context.Entry(single).State = state; - Assert.True(navigationEntry.IsLoaded); + if (state != EntityState.Deleted) // FK is null + { + Assert.True(navigationEntry.IsLoaded); - // Issue #16429 - var parent = async - ? (await navigationEntry.Query().ToListAsync()).Single() - : navigationEntry.Query().ToList().Single(); + // Issue #16429 + var parent = async + ? (await navigationEntry.Query().ToListAsync()).Single() + : navigationEntry.Query().ToList().Single(); - Assert.True(navigationEntry.IsLoaded); + Assert.True(navigationEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.NotNull(parent); - Assert.Same(parent, single.Parent); - Assert.Same(single, ((Parent)parent).Single); + Assert.NotNull(parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Same(parent, single.Parent); + Assert.Same(single, ((Parent)parent).Single); + } + } + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] @@ -4092,12 +3061,16 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_alr [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_already_loaded_untyped( EntityState state, bool async, @@ -4113,6 +3086,7 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_alr var navigationEntry = context.Entry(parent).Navigation("Single"); + context.Entry(parent.Single).State = state; context.Entry(parent).State = state; Assert.True(navigationEntry.IsLoaded); @@ -4127,20 +3101,32 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_alr RecordLog(); Assert.NotNull(single); - Assert.Same(single, parent.Single); - - Assert.Same(parent, ((Single)single).Parent); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state == EntityState.Detached) + { + Assert.NotSame(single, parent.Single); + Assert.Null(((Single)single).Parent); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Same(single, parent.Single); + Assert.Same(parent, ((Single)single).Parent); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4170,16 +3156,20 @@ public virtual async Task Load_collection_alternate_key(EntityState state, bool Assert.Equal(2, parent.ChildrenAk.Count()); Assert.All(parent.ChildrenAk.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4206,29 +3196,36 @@ public virtual async Task Load_many_to_one_reference_to_principal_alternate_key( RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenAk); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenAk.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenAk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenAk.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4255,29 +3252,36 @@ public virtual async Task Load_one_to_one_reference_to_principal_alternate_key(E RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) + if (state != EntityState.Detached) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleAk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleAk); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleAk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4304,21 +3308,28 @@ public virtual async Task Load_one_to_one_reference_to_dependent_alternate_key(E RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleAk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4341,20 +3352,33 @@ public virtual async Task Load_collection_using_Query_alternate_key(EntityState RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.ChildrenAk.Count()); - Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.ChildrenAk)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + if (state == EntityState.Detached) + { + Assert.Null(parent.Children); + Assert.All(children, c => Assert.Null(c.Parent)); + Assert.Equal(0, context.ChangeTracker.Entries().Count()); + } + else + { + Assert.Equal(2, parent.ChildrenAk.Count()); + Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.ChildrenAk)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4372,33 +3396,40 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_al ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) + if (state != EntityState.Detached) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenAk); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenAk.Single()); - } + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenAk); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenAk.Single()); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4416,33 +3447,40 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_alt ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) + if (state != EntityState.Detached) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleAk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleAk); - } + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + Assert.Null(parent.SingleAk); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_alternate_key(EntityState state, bool async) { using var context = CreateContext(); @@ -4460,29 +3498,36 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_alt ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.NotNull(single); - RecordLog(); + if (state != EntityState.Detached) + { + Assert.True(referenceEntry.IsLoaded); - Assert.NotNull(single); - Assert.Same(single, parent.SingleAk); - Assert.Same(parent, single.Parent); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Same(single, parent.SingleAk); + Assert.Same(parent, single.Parent); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_null_FK_alternate_key(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildAk { Id = 767, ParentId = null }).Entity; + var child = context.Attach(new ChildAk { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -4505,22 +3550,25 @@ public virtual async Task Load_many_to_one_reference_to_principal_null_FK_altern RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(child.Parent); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_null_FK_alternate_key(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleAk { Id = 767, ParentId = null }).Entity; + var single = context.Attach(new SingleAk { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -4543,7 +3591,7 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK_alterna RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(single.Parent); } @@ -4551,15 +3599,18 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK_alterna [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_alternate_key(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildAk { Id = 767, ParentId = null }).Entity; + var child = context.Attach(new ChildAk { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -4580,21 +3631,24 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_nu Assert.Null(parent); Assert.Null(child.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_alternate_key(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleAk { Id = 767, ParentId = null }).Entity; + var single = context.Attach(new SingleAk { Id = 767, ParentId = null }).Entity; ClearLog(); @@ -4615,16 +3669,20 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_nul Assert.Null(parent); Assert.Null(single.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4654,16 +3712,20 @@ public virtual async Task Load_collection_shadow_fk(EntityState state, bool asyn Assert.Equal(2, parent.ChildrenShadowFk.Count()); Assert.All(parent.ChildrenShadowFk.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4677,42 +3739,64 @@ public virtual async Task Load_many_to_one_reference_to_principal_shadow_fk(Enti Assert.False(referenceEntry.IsLoaded); - if (async) + if (state == EntityState.Detached) { - await referenceEntry.LoadAsync(); + if (async) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + (await Assert.ThrowsAsync(() => referenceEntry.LoadAsync())).Message); + } + else + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => referenceEntry.Load()).Message); + } } else { - referenceEntry.Load(); - } + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + var parent = context.ChangeTracker.Entries().Single().Entity; - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenShadowFk); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenShadowFk.Single()); + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenShadowFk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenShadowFk.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4726,42 +3810,66 @@ public virtual async Task Load_one_to_one_reference_to_principal_shadow_fk(Entit Assert.False(referenceEntry.IsLoaded); - if (async) + if (state == EntityState.Detached) { - await referenceEntry.LoadAsync(); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + (await Assert.ThrowsAsync( + async () => + { + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } + })).Message); } else { - referenceEntry.Load(); - } + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var parent = context.ChangeTracker.Entries().Single().Entity; + var parent = context.ChangeTracker.Entries().Single().Entity; - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleShadowFk); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleShadowFk); + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleShadowFk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4788,21 +3896,28 @@ public virtual async Task Load_one_to_one_reference_to_dependent_shadow_fk(Entit RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleShadowFk); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4825,20 +3940,33 @@ public virtual async Task Load_collection_using_Query_shadow_fk(EntityState stat RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.ChildrenShadowFk.Count()); - Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.ChildrenShadowFk)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + if (state == EntityState.Detached) + { + Assert.Empty(parent.ChildrenShadowFk); + Assert.All(children, c => Assert.Null(c.Parent)); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Equal(2, parent.ChildrenShadowFk.Count()); + Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.ChildrenShadowFk)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4852,37 +3980,56 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_sh Assert.False(referenceEntry.IsLoaded); - var parent = async - ? await referenceEntry.Query().SingleAsync() - : referenceEntry.Query().Single(); - - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - - RecordLog(); - - Assert.NotNull(parent); - - if (state == EntityState.Deleted) + if (state == EntityState.Detached) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenShadowFk); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + (await Assert.ThrowsAsync( + async () => + { + _ = async + ? await referenceEntry.Query().SingleOrDefaultAsync() + : referenceEntry.Query().SingleOrDefault(); + })).Message); } else { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenShadowFk.Single()); - } + var parent = async + ? await referenceEntry.Query().SingleAsync() + : referenceEntry.Query().Single(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); + + RecordLog(); + + Assert.NotNull(parent); + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenShadowFk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenShadowFk.Single()); + } + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4896,37 +4043,56 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_sha Assert.False(referenceEntry.IsLoaded); - var parent = async - ? await referenceEntry.Query().SingleAsync() - : referenceEntry.Query().Single(); - - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - - RecordLog(); - - Assert.NotNull(parent); - - if (state == EntityState.Deleted) + if (state == EntityState.Detached) { - Assert.Null(single.Parent); - Assert.Null(parent.SingleShadowFk); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + (await Assert.ThrowsAsync( + async () => + { + _ = async + ? await referenceEntry.Query().SingleAsync() + : referenceEntry.Query().Single(); + })).Message); } else { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleShadowFk); - } + var parent = async + ? await referenceEntry.Query().SingleAsync() + : referenceEntry.Query().Single(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); + + RecordLog(); + + Assert.NotNull(parent); + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleShadowFk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + } + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); @@ -4944,29 +4110,37 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_sha ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.NotNull(single); - RecordLog(); + if (state != EntityState.Detached) + { + Assert.True(referenceEntry.IsLoaded); - Assert.NotNull(single); - Assert.Same(single, parent.SingleShadowFk); - Assert.Same(parent, single.Parent); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Same(single, parent.SingleShadowFk); + Assert.Same(parent, single.Parent); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildShadowFk { Id = 767 }).Entity; + var child = context.Attach(new ChildShadowFk { Id = 767 }).Entity; + context.Entry(child).Property("ParentId").CurrentValue = null; ClearLog(); @@ -4976,35 +4150,57 @@ public virtual async Task Load_many_to_one_reference_to_principal_null_FK_shadow Assert.False(referenceEntry.IsLoaded); - if (async) + if (state == EntityState.Detached) { - await referenceEntry.LoadAsync(); + if (async) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + (await Assert.ThrowsAsync(() => referenceEntry.LoadAsync())).Message); + } + else + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => referenceEntry.Load()).Message); + } } else { - referenceEntry.Load(); - } + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); - Assert.Null(child.Parent); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleShadowFk { Id = 767 }).Entity; + var single = context.Attach(new SingleShadowFk { Id = 767 }).Entity; + context.Entry(single).Property("ParentId").CurrentValue = null; ClearLog(); @@ -5014,36 +4210,60 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK_shadow_ Assert.False(referenceEntry.IsLoaded); - if (async) + if (state == EntityState.Detached) { - await referenceEntry.LoadAsync(); + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + (await Assert.ThrowsAsync( + async () => + { + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } + })).Message); } else { - referenceEntry.Load(); - } + if (async) + { + await referenceEntry.LoadAsync(); + } + else + { + referenceEntry.Load(); + } - Assert.True(referenceEntry.IsLoaded); + Assert.True(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); - Assert.Null(single.Parent); + Assert.Null(single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildShadowFk { Id = 767 }).Entity; + var child = context.Attach(new ChildShadowFk { Id = 767 }).Entity; + context.Entry(child).Property("ParentId").CurrentValue = null; ClearLog(); @@ -5053,32 +4273,51 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_nu Assert.False(referenceEntry.IsLoaded); - var parent = async - ? await referenceEntry.Query().SingleOrDefaultAsync() - : referenceEntry.Query().SingleOrDefault(); + if (state == EntityState.Detached) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + (await Assert.ThrowsAsync( + async () => + { + _ = async + ? await referenceEntry.Query().SingleOrDefaultAsync() + : referenceEntry.Query().SingleOrDefault(); + })).Message); + } + else + { + var parent = async + ? await referenceEntry.Query().SingleOrDefaultAsync() + : referenceEntry.Query().SingleOrDefault(); - Assert.False(referenceEntry.IsLoaded); + Assert.False(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.Null(parent); - Assert.Null(child.Parent); + Assert.Null(parent); + Assert.Null(child.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Single(context.ChangeTracker.Entries()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleShadowFk { Id = 767 }).Entity; + var single = context.Attach(new SingleShadowFk { Id = 767 }).Entity; + context.Entry(single).Property("ParentId").CurrentValue = null; ClearLog(); @@ -5088,27 +4327,46 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_nul Assert.False(referenceEntry.IsLoaded); - var parent = async - ? await referenceEntry.Query().SingleOrDefaultAsync() - : referenceEntry.Query().SingleOrDefault(); + if (state == EntityState.Detached) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + (await Assert.ThrowsAsync( + async () => + { + _ = async + ? await referenceEntry.Query().SingleOrDefaultAsync() + : referenceEntry.Query().SingleOrDefault(); + })).Message); + } + else + { + var parent = async + ? await referenceEntry.Query().SingleOrDefaultAsync() + : referenceEntry.Query().SingleOrDefault(); - Assert.False(referenceEntry.IsLoaded); + Assert.False(referenceEntry.IsLoaded); - RecordLog(); + RecordLog(); - Assert.Null(parent); - Assert.Null(single.Parent); + Assert.Null(parent); + Assert.Null(single.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Single(context.ChangeTracker.Entries()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5138,16 +4396,20 @@ public virtual async Task Load_collection_composite_key(EntityState state, bool Assert.Equal(2, parent.ChildrenCompositeKey.Count()); Assert.All(parent.ChildrenCompositeKey.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5174,29 +4436,36 @@ public virtual async Task Load_many_to_one_reference_to_principal_composite_key( RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenCompositeKey); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenCompositeKey.Single()); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenCompositeKey); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenCompositeKey.Single()); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5223,29 +4492,36 @@ public virtual async Task Load_one_to_one_reference_to_principal_composite_key(E RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - var parent = context.ChangeTracker.Entries().Single().Entity; + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleCompositeKey); - } - else + if (state != EntityState.Detached) { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleCompositeKey); + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleCompositeKey); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + } } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5272,21 +4548,28 @@ public virtual async Task Load_one_to_one_reference_to_dependent_composite_key(E RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + if (state != EntityState.Detached) + { + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - var single = context.ChangeTracker.Entries().Single().Entity; + var single = context.ChangeTracker.Entries().Single().Entity; - Assert.Same(single, parent.SingleCompositeKey); - Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + Assert.Same(parent, single.Parent); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5309,20 +4592,33 @@ public virtual async Task Load_collection_using_Query_composite_key(EntityState RecordLog(); Assert.Equal(2, children.Count); - Assert.Equal(2, parent.ChildrenCompositeKey.Count()); - Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); - Assert.All(children, p => Assert.Contains(p, parent.ChildrenCompositeKey)); - Assert.Equal(3, context.ChangeTracker.Entries().Count()); + if (state == EntityState.Detached) + { + Assert.Empty(parent.ChildrenCompositeKey); + Assert.All(children, c => Assert.Null(c.Parent)); + Assert.Empty(context.ChangeTracker.Entries()); + } + else + { + Assert.Equal(2, parent.ChildrenCompositeKey.Count()); + Assert.All(children.Select(e => e.Parent), c => Assert.Same(parent, c)); + Assert.All(children, p => Assert.Contains(p, parent.ChildrenCompositeKey)); + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5340,33 +4636,40 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_co ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) + if (state != EntityState.Detached) { - Assert.Null(child.Parent); - Assert.Null(parent.ChildrenCompositeKey); - } - else - { - Assert.Same(parent, child.Parent); - Assert.Same(child, parent.ChildrenCompositeKey.Single()); - } + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenCompositeKey); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenCompositeKey.Single()); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5384,33 +4687,40 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_com ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.Equal(state != EntityState.Deleted, referenceEntry.IsLoaded); - RecordLog(); Assert.NotNull(parent); - if (state == EntityState.Deleted) - { - Assert.Null(single.Parent); - Assert.Null(parent.SingleCompositeKey); - } - else - { - Assert.Same(parent, single.Parent); - Assert.Same(single, parent.SingleCompositeKey); - } + if (state != EntityState.Detached) + { + if (state == EntityState.Deleted) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + Assert.Null(parent.SingleCompositeKey); + } + else + { + Assert.True(referenceEntry.IsLoaded); + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + } - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_composite_key(EntityState state, bool async) { using var context = CreateContext(); @@ -5428,29 +4738,36 @@ public virtual async Task Load_one_to_one_reference_to_dependent_using_Query_com ? await referenceEntry.Query().SingleAsync() : referenceEntry.Query().Single(); - Assert.True(referenceEntry.IsLoaded); + Assert.NotNull(single); - RecordLog(); + if (state != EntityState.Detached) + { + Assert.True(referenceEntry.IsLoaded); - Assert.NotNull(single); - Assert.Same(single, parent.SingleCompositeKey); - Assert.Same(parent, single.Parent); + RecordLog(); - Assert.Equal(2, context.ChangeTracker.Entries().Count()); + Assert.Same(single, parent.SingleCompositeKey); + Assert.Same(parent, single.Parent); + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_null_FK_composite_key(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildCompositeKey { Id = 767, ParentId = 567 }).Entity; + var child = context.Attach(new ChildCompositeKey { Id = 767, ParentId = 567 }).Entity; ClearLog(); @@ -5473,22 +4790,25 @@ public virtual async Task Load_many_to_one_reference_to_principal_null_FK_compos RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(child.Parent); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_null_FK_composite_key(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleCompositeKey { Id = 767, ParentAlternateId = "Boot" }).Entity; + var single = context.Attach(new SingleCompositeKey { Id = 767, ParentAlternateId = "Boot" }).Entity; ClearLog(); @@ -5511,7 +4831,7 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK_composi RecordLog(); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); Assert.Null(single.Parent); } @@ -5519,15 +4839,18 @@ public virtual async Task Load_one_to_one_reference_to_principal_null_FK_composi [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_composite_key(EntityState state, bool async) { using var context = CreateContext(); - var child = context.Attach( - new ChildCompositeKey { Id = 767, ParentAlternateId = "Boot" }).Entity; + var child = context.Attach(new ChildCompositeKey { Id = 767, ParentAlternateId = "Boot" }).Entity; ClearLog(); @@ -5548,21 +4871,24 @@ public virtual async Task Load_many_to_one_reference_to_principal_using_Query_nu Assert.Null(parent); Assert.Null(child.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_one_to_one_reference_to_principal_using_Query_null_FK_composite_key(EntityState state, bool async) { using var context = CreateContext(); - var single = context.Attach( - new SingleCompositeKey { Id = 767, ParentId = 567 }).Entity; + var single = context.Attach(new SingleCompositeKey { Id = 767, ParentId = 567 }).Entity; ClearLog(); @@ -5583,7 +4909,7 @@ public virtual async Task Load_one_to_one_reference_to_principal_using_Query_nul Assert.Null(parent); Assert.Null(single.Parent); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalFact] @@ -5623,513 +4949,36 @@ public virtual void Can_change_IsLoaded_flag_for_collection() [ConditionalFact] public virtual void Can_change_IsLoaded_flag_for_reference_only_if_null() { - using var context = CreateContext(); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - Assert.False(referenceEntry.IsLoaded); - - referenceEntry.IsLoaded = true; - - Assert.True(referenceEntry.IsLoaded); - - referenceEntry.Load(); - - Assert.True(referenceEntry.IsLoaded); - - Assert.Single(context.ChangeTracker.Entries()); - - referenceEntry.IsLoaded = true; - - referenceEntry.IsLoaded = false; - - referenceEntry.Load(); - - Assert.Equal(2, context.ChangeTracker.Entries().Count()); - - Assert.True(referenceEntry.IsLoaded); - - Assert.Equal( - CoreStrings.ReferenceMustBeLoaded("Parent", typeof(Child).Name), - Assert.Throws(() => referenceEntry.IsLoaded = false).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_collection_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var collectionEntry = context.Entry(parent).Collection(e => e.Children); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_collection_using_string_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var collectionEntry = context.Entry(parent).Collection(nameof(Parent.Children)); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_collection_with_navigation_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var collectionEntry = context.Entry(parent).Navigation(nameof(Parent.Children)); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_reference_to_principal_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_reference_with_navigation_to_principal_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Navigation(nameof(Child.Parent)); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_reference_using_string_to_principal_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Reference(nameof(Child.Parent)); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_reference_to_dependent_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var referenceEntry = context.Entry(parent).Reference(e => e.Single); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_reference_to_dependent_with_navigation_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var referenceEntry = context.Entry(parent).Navigation(nameof(Parent.Single)); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true, false)] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(false, true)] - public virtual async Task Load_reference_to_dependent_using_string_for_detached_throws(bool async, bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var referenceEntry = context.Entry(parent).Reference(nameof(Parent.Single)); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await referenceEntry.LoadAsync(); - } - else - { - referenceEntry.Load(); - } - })).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_collection_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var collectionEntry = context.Entry(parent).Collection(e => e.Children); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - Assert.Throws(() => collectionEntry.Query()).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_collection_using_string_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var collectionEntry = context.Entry(parent).Collection(nameof(Parent.Children)); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - Assert.Throws(() => collectionEntry.Query()).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_collection_with_navigation_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); - - var collectionEntry = context.Entry(parent).Navigation(nameof(Parent.Children)); - - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Children), nameof(Parent)), - Assert.Throws(() => collectionEntry.Query()).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_reference_to_principal_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Reference(e => e.Parent); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - Assert.Throws(() => referenceEntry.Query()).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_reference_with_navigation_to_principal_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Navigation(nameof(Child.Parent)); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } - - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - Assert.Throws(() => referenceEntry.Query()).Message); - } - - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_reference_using_string_to_principal_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var child = context.Set().Single(e => e.Id == 12); - - var referenceEntry = context.Entry(child).Reference(nameof(Child.Parent)); - - if (!noTracking) - { - context.Entry(child).State = EntityState.Detached; - } + using var context = CreateContext(); + var child = context.Set().Single(e => e.Id == 12); - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Child.Parent), nameof(Child)), - Assert.Throws(() => referenceEntry.Query()).Message); - } + var referenceEntry = context.Entry(child).Reference(e => e.Parent); - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_reference_to_dependent_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); + Assert.False(referenceEntry.IsLoaded); - var referenceEntry = context.Entry(parent).Reference(e => e.Single); + referenceEntry.IsLoaded = true; - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } + Assert.True(referenceEntry.IsLoaded); - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - Assert.Throws(() => referenceEntry.Query()).Message); - } + referenceEntry.Load(); - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_reference_to_dependent_with_navigation_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); + Assert.True(referenceEntry.IsLoaded); - var referenceEntry = context.Entry(parent).Navigation(nameof(Parent.Single)); + Assert.Single(context.ChangeTracker.Entries()); - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } + referenceEntry.IsLoaded = true; - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - Assert.Throws(() => referenceEntry.Query()).Message); - } + referenceEntry.IsLoaded = false; - [ConditionalTheory] - [InlineData(true)] - [InlineData(false)] - public virtual void Query_reference_to_dependent_using_string_for_detached_throws(bool noTracking) - { - using var context = CreateContext(noTracking: noTracking); - var parent = context.Set().Single(); + referenceEntry.Load(); - var referenceEntry = context.Entry(parent).Reference(nameof(Parent.Single)); + Assert.Equal(2, context.ChangeTracker.Entries().Count()); - if (!noTracking) - { - context.Entry(parent).State = EntityState.Detached; - } + Assert.True(referenceEntry.IsLoaded); Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(Parent.Single), nameof(Parent)), - Assert.Throws(() => referenceEntry.Query()).Message); + CoreStrings.ReferenceMustBeLoaded("Parent", typeof(Child).Name), + Assert.Throws(() => referenceEntry.IsLoaded = false).Message); } [ConditionalFact] // Issue #27497 @@ -6188,9 +5037,22 @@ public virtual void Setting_navigation_to_null_is_detected_by_local_DetectChange Assert.Equal(EntityState.Deleted, childEntry.State); } + private static void SetState( + DbContext context, + object entity, + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool isAttached = false) + { + if (isAttached && state == EntityState.Detached + || state != (queryTrackingBehavior == QueryTrackingBehavior.TrackAll ? EntityState.Unchanged : EntityState.Detached)) + { + context.Entry(entity).State = state; + } + } + protected class Parent { - private readonly Action _loader; private IEnumerable _children; private SinglePkToPk _singlePkToPk; private Single _single; @@ -6202,14 +5064,7 @@ protected class Parent private IEnumerable _childrenCompositeKey; private SingleCompositeKey _singleCompositeKey; - public Parent() - { - } - - public Parent(Action lazyLoader) - { - _loader = lazyLoader; - } + public ILazyLoader Loader { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } @@ -6218,78 +5073,137 @@ public Parent(Action lazyLoader) public IEnumerable Children { - get => _loader.Load(this, ref _children); + get => Loader.Load(this, ref _children); set => _children = value; } + public async Task> LazyLoadChildren(bool async) + { + if (async) + { + await Loader.LoadAsync(this, default, nameof(Children)); + return _children; + } + + return Children; + } + public SinglePkToPk SinglePkToPk { - get => _loader.Load(this, ref _singlePkToPk); + get => Loader.Load(this, ref _singlePkToPk); set => _singlePkToPk = value; } public Single Single { - get => _loader.Load(this, ref _single); + get => Loader.Load(this, ref _single); set => _single = value; } + public async Task LazyLoadSingle(bool async) + { + if (async) + { + await Loader.LoadAsync(this, default, nameof(Single)); + return _single; + } + + return Single; + } + public RequiredSingle RequiredSingle { - get => _loader.Load(this, ref _requiredSingle); + get => Loader.Load(this, ref _requiredSingle); set => _requiredSingle = value; } public IEnumerable ChildrenAk { - get => _loader.Load(this, ref _childrenAk); + get => Loader.Load(this, ref _childrenAk); set => _childrenAk = value; } public SingleAk SingleAk { - get => _loader.Load(this, ref _singleAk); + get => Loader.Load(this, ref _singleAk); set => _singleAk = value; } public IEnumerable ChildrenShadowFk { - get => _loader.Load(this, ref _childrenShadowFk); + get => Loader.Load(this, ref _childrenShadowFk); set => _childrenShadowFk = value; } public SingleShadowFk SingleShadowFk { - get => _loader.Load(this, ref _singleShadowFk); + get => Loader.Load(this, ref _singleShadowFk); set => _singleShadowFk = value; } public IEnumerable ChildrenCompositeKey { - get => _loader.Load(this, ref _childrenCompositeKey); + get => Loader.Load(this, ref _childrenCompositeKey); set => _childrenCompositeKey = value; } public SingleCompositeKey SingleCompositeKey { - get => _loader.Load(this, ref _singleCompositeKey); + get => Loader.Load(this, ref _singleCompositeKey); set => _singleCompositeKey = value; } } protected class Child { - private readonly Action _loader; private Parent _parent; - public Child() + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId { get; set; } + + public Parent Parent { + get => Loader.Load(this, ref _parent); + set => _parent = value; } - public Child(Action lazyLoader) + public async Task LazyLoadParent(bool async) { - _loader = lazyLoader; + if (async) + { + await Loader.LoadAsync(this, default, nameof(Parent)); + return _parent; + } + + return Parent; + } + } + + protected class SinglePkToPk + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; } + } + + protected class Single + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } @@ -6298,197 +5212,392 @@ public Child(Action lazyLoader) public Parent Parent { - get => _loader.Load(this, ref _parent); + get => Loader.Load(this, ref _parent); set => _parent = value; } + + public async Task LazyLoadParent(bool async) + { + if (async) + { + await Loader.LoadAsync(this, default, nameof(Parent)); + return _parent; + } + + return Parent; + } } - protected class SinglePkToPk + protected class RequiredSingle + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int ParentId { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class ChildAk + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string ParentId { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class SingleAk + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string ParentId { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class ChildShadowFk + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class SingleShadowFk + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class ChildCompositeKey + { + private Parent _parent; + + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId { get; set; } + public string ParentAlternateId { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected class SingleCompositeKey { - private readonly Action _loader; private Parent _parent; - public SinglePkToPk() + private ILazyLoader Loader { get; set; } + + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId { get; set; } + public string ParentAlternateId { get; set; } + + public Parent Parent + { + get => Loader.Load(this, ref _parent); + set => _parent = value; + } + } + + protected abstract class RootClass + { + protected RootClass(Action lazyLoader) + { + LazyLoader = lazyLoader; + } + + protected RootClass() + { + } + + public int Id { get; set; } + + protected Action LazyLoader { get; } + } + + protected class Deposit : RootClass + { + private Deposit(Action lazyLoader) + : base(lazyLoader) + { + } + + public Deposit() + { + } + } + + protected abstract class Product : RootClass + { + protected Product(Action lazyLoader) + : base(lazyLoader) + { + } + + protected Product() + { + } + + public int? DepositID { get; set; } + + private Deposit _deposit; + + public Deposit Deposit { + get => LazyLoader.Load(this, ref _deposit); + set => _deposit = value; } + } - protected SinglePkToPk(Action lazyLoader) + protected class SimpleProduct : Product + { + private SimpleProduct(Action lazyLoader) + : base(lazyLoader) { - _loader = lazyLoader; } - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public Parent Parent + public SimpleProduct() { - get => _loader.Load(this, ref _parent); - set => _parent = value; } } - protected class Single + protected class OptionalChildView { private readonly Action _loader; - private Parent _parent; + private RootClass _root; - public Single() + public OptionalChildView() { } - public Single(Action lazyLoader) + public OptionalChildView(Action lazyLoader) { _loader = lazyLoader; } - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public int? ParentId { get; set; } + public int? RootId { get; set; } - public Parent Parent + public RootClass Root { - get => _loader.Load(this, ref _parent); - set => _parent = value; + get => _loader.Load(this, ref _root); + set => _root = value; } } - protected class RequiredSingle + protected class RequiredChildView { private readonly Action _loader; - private Parent _parent; + private RootClass _root; - public RequiredSingle() + public RequiredChildView() { } - public RequiredSingle(Action lazyLoader) + public RequiredChildView(Action lazyLoader) { _loader = lazyLoader; } - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public int ParentId { get; set; } + public int RootId { get; set; } - public Parent Parent + public RootClass Root { - get => _loader.Load(this, ref _parent); - set => _parent = value; + get => _loader.Load(this, ref _root); + set => _root = value; } } - protected class ChildAk + protected class ParentFullLoaderByConstructor { - private readonly Action _loader; - private Parent _parent; + private readonly ILazyLoader _loader; + private IEnumerable _children; + private SingleFullLoaderByConstructor _single; - public ChildAk() + public ParentFullLoaderByConstructor() { } - public ChildAk(Action lazyLoader) + private ParentFullLoaderByConstructor(ILazyLoader loader) { - _loader = lazyLoader; + _loader = loader; } [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } - public string ParentId { get; set; } - - public Parent Parent + public IEnumerable Children { - get => _loader.Load(this, ref _parent); - set => _parent = value; + get => _loader.Load(this, ref _children); + set => _children = value; } - } - - protected class SingleAk - { - private readonly Action _loader; - private Parent _parent; - public SingleAk() + public async Task> LazyLoadChildren(bool async) { + if (async) + { + await _loader.LoadAsync(this, default, nameof(Children)); + return _children; + } + + return Children; } - public SingleAk(Action lazyLoader) + public SingleFullLoaderByConstructor Single { - _loader = lazyLoader; + get => _loader.Load(this, ref _single); + set => _single = value; } - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public string ParentId { get; set; } - - public Parent Parent + public async Task LazyLoadSingle(bool async) { - get => _loader.Load(this, ref _parent); - set => _parent = value; + if (async) + { + await _loader.LoadAsync(this, default, nameof(Single)); + return _single; + } + + return Single; } } - protected class ChildShadowFk + protected class ChildFullLoaderByConstructor { - private readonly Action _loader; - private Parent _parent; + private readonly ILazyLoader _loader; + private ParentFullLoaderByConstructor _parent; - public ChildShadowFk() + public ChildFullLoaderByConstructor() { } - public ChildShadowFk(Action lazyLoader) + public ChildFullLoaderByConstructor(ILazyLoader loader) { - _loader = lazyLoader; + _loader = loader; } [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } - public Parent Parent + public int? ParentId { get; set; } + + public ParentFullLoaderByConstructor Parent { get => _loader.Load(this, ref _parent); set => _parent = value; } + + public async Task LazyLoadParent(bool async) + { + if (async) + { + await _loader.LoadAsync(this, default, nameof(Parent)); + return _parent; + } + + return Parent; + } } - protected class SingleShadowFk + protected class SingleFullLoaderByConstructor { - private readonly Action _loader; - private Parent _parent; + private readonly ILazyLoader _loader; + private ParentFullLoaderByConstructor _parent; - public SingleShadowFk() + public SingleFullLoaderByConstructor() { } - public SingleShadowFk(Action lazyLoader) + public SingleFullLoaderByConstructor(ILazyLoader loader) { - _loader = lazyLoader; + _loader = loader; } [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } - public Parent Parent + public int? ParentId { get; set; } + + public ParentFullLoaderByConstructor Parent { get => _loader.Load(this, ref _parent); set => _parent = value; } + + public async Task LazyLoadParent(bool async) + { + if (async) + { + await _loader.LoadAsync(this, default, nameof(Parent)); + return _parent; + } + + return Parent; + } } - protected class ChildCompositeKey + protected class ParentDelegateLoaderByConstructor { private readonly Action _loader; - private Parent _parent; + private IEnumerable _children; + private SingleDelegateLoaderByConstructor _single; - public ChildCompositeKey() + public ParentDelegateLoaderByConstructor() { } - public ChildCompositeKey(Action lazyLoader) + private ParentDelegateLoaderByConstructor(Action lazyLoader) { _loader = lazyLoader; } @@ -6496,26 +5605,30 @@ public ChildCompositeKey(Action lazyLoader) [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } - public int? ParentId { get; set; } - public string ParentAlternateId { get; set; } + public IEnumerable Children + { + get => _children ?? _loader.Load(this, ref _children); + set => _children = value; + } - public Parent Parent + public SingleDelegateLoaderByConstructor Single { - get => _loader.Load(this, ref _parent); - set => _parent = value; + get => _single ?? _loader.Load(this, ref _single); + set => _single = value; } } - protected class SingleCompositeKey + protected class ChildDelegateLoaderByConstructor { private readonly Action _loader; - private Parent _parent; + private ParentDelegateLoaderByConstructor _parent; + private int? _parentId; - public SingleCompositeKey() + public ChildDelegateLoaderByConstructor() { } - public SingleCompositeKey(Action lazyLoader) + private ChildDelegateLoaderByConstructor(Action lazyLoader) { _loader = lazyLoader; } @@ -6523,121 +5636,144 @@ public SingleCompositeKey(Action lazyLoader) [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } - public int? ParentId { get; set; } - public string ParentAlternateId { get; set; } + public int? ParentId + { + get => _parentId; + set + { + if (_parentId != value) + { + _parentId = value; + _parent = null; + } + } + } - public Parent Parent + public ParentDelegateLoaderByConstructor Parent { - get => _loader.Load(this, ref _parent); + get => _parent ?? _loader.Load(this, ref _parent); set => _parent = value; } } - protected abstract class RootClass + protected class SingleDelegateLoaderByConstructor { - protected RootClass(Action lazyLoader) + private readonly Action _loader; + private ParentDelegateLoaderByConstructor _parent; + private int? _parentId; + + public SingleDelegateLoaderByConstructor() { - LazyLoader = lazyLoader; } - protected RootClass() + private SingleDelegateLoaderByConstructor(Action lazyLoader) { + _loader = lazyLoader; } + [DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { get; set; } - protected Action LazyLoader { get; } - } - - protected class Deposit : RootClass - { - private Deposit(Action lazyLoader) - : base(lazyLoader) + public int? ParentId { + get => _parentId; + set + { + if (_parentId != value) + { + _parentId = value; + _parent = null; + } + } } - public Deposit() + public ParentDelegateLoaderByConstructor Parent { + get => _parent ?? _loader.Load(this, ref _parent); + set => _parent = value; } } - protected abstract class Product : RootClass + protected class ParentDelegateLoaderByProperty { - protected Product(Action lazyLoader) - : base(lazyLoader) - { - } - - protected Product() - { - } - - public int? DepositID { get; set; } + private IEnumerable _children; + private SingleDelegateLoaderByProperty _single; - private Deposit _deposit; + private Action LazyLoader { get; set; } - public Deposit Deposit - { - get => LazyLoader.Load(this, ref _deposit); - set => _deposit = value; - } - } + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } - protected class SimpleProduct : Product - { - private SimpleProduct(Action lazyLoader) - : base(lazyLoader) + public IEnumerable Children { + get => _children ?? LazyLoader.Load(this, ref _children); + set => _children = value; } - public SimpleProduct() + public SingleDelegateLoaderByProperty Single { + get => _single ?? LazyLoader.Load(this, ref _single); + set => _single = value; } } - protected class OptionalChildView + protected class ChildDelegateLoaderByProperty { - private readonly Action _loader; - private RootClass _root; + private ParentDelegateLoaderByProperty _parent; + private int? _parentId; - public OptionalChildView() - { - } + private Action LazyLoader { get; set; } - public OptionalChildView(Action lazyLoader) + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId { - _loader = lazyLoader; + get => _parentId; + set + { + if (_parentId != value) + { + _parentId = value; + _parent = null; + } + } } - public int? RootId { get; set; } - - public RootClass Root + public ParentDelegateLoaderByProperty Parent { - get => _loader.Load(this, ref _root); - set => _root = value; + get => _parent ?? LazyLoader.Load(this, ref _parent); + set => _parent = value; } } - protected class RequiredChildView + protected class SingleDelegateLoaderByProperty { - private readonly Action _loader; - private RootClass _root; + private ParentDelegateLoaderByProperty _parent; + private int? _parentId; - public RequiredChildView() - { - } + private Action LazyLoader { get; set; } - public RequiredChildView(Action lazyLoader) + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public int? ParentId { - _loader = lazyLoader; + get => _parentId; + set + { + if (_parentId != value) + { + _parentId = value; + _parent = null; + } + } } - public int RootId { get; set; } - - public RootClass Root + public ParentDelegateLoaderByProperty Parent { - get => _loader.Load(this, ref _root); - set => _root = value; + get => _parent ?? LazyLoader.Load(this, ref _parent); + set => _parent = value; } } @@ -6750,6 +5886,42 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con e => new { e.ParentAlternateId, e.ParentId }); }); + modelBuilder.Entity( + b => + { + b.HasMany(nameof(ParentFullLoaderByConstructor.Children)) + .WithOne(nameof(ChildFullLoaderByConstructor.Parent)) + .HasForeignKey(e => e.ParentId); + + b.HasOne(nameof(ParentFullLoaderByConstructor.Single)) + .WithOne(e => e.Parent) + .HasForeignKey(e => e.ParentId); + }); + + modelBuilder.Entity( + b => + { + b.HasMany(nameof(ParentDelegateLoaderByConstructor.Children)) + .WithOne(nameof(ChildDelegateLoaderByConstructor.Parent)) + .HasForeignKey(e => e.ParentId); + + b.HasOne(nameof(ParentDelegateLoaderByConstructor.Single)) + .WithOne(e => e.Parent) + .HasForeignKey(e => e.ParentId); + }); + + modelBuilder.Entity( + b => + { + b.HasMany(nameof(ParentDelegateLoaderByProperty.Children)) + .WithOne(nameof(ChildDelegateLoaderByProperty.Parent)) + .HasForeignKey(e => e.ParentId); + + b.HasOne(nameof(ParentDelegateLoaderByProperty.Single)) + .WithOne(e => e.Parent) + .HasForeignKey(e => e.ParentId); + }); + modelBuilder.Entity(); modelBuilder.Entity(); modelBuilder.Entity(); @@ -6778,6 +5950,30 @@ protected override void Seed(PoolableDbContext context) SingleCompositeKey = new SingleCompositeKey { Id = 62 } }); + context.Add( + new ParentFullLoaderByConstructor + { + Id = 707, + Children = new List { new() { Id = 11 }, new() { Id = 12 } }, + Single = new SingleFullLoaderByConstructor { Id = 21 } + }); + + context.Add( + new ParentDelegateLoaderByConstructor + { + Id = 707, + Children = new List { new() { Id = 11 }, new() { Id = 12 } }, + Single = new SingleDelegateLoaderByConstructor { Id = 21 } + }); + + context.Add( + new ParentDelegateLoaderByProperty + { + Id = 707, + Children = new List { new() { Id = 11 }, new() { Id = 12 } }, + Single = new SingleDelegateLoaderByProperty { Id = 21 } + }); + context.Add( new SimpleProduct { Deposit = new Deposit() }); diff --git a/test/EFCore.Specification.Tests/ManyToManyFieldsLoadTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyFieldsLoadTestBase.cs index 96e7e968134..63a9d084ab2 100644 --- a/test/EFCore.Specification.Tests/ManyToManyFieldsLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/ManyToManyFieldsLoadTestBase.cs @@ -643,20 +643,14 @@ public virtual async Task Load_collection_for_detached_throws(bool async, QueryT context.Entry(left).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(EntityOne)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } [ConditionalTheory] @@ -676,9 +670,7 @@ public virtual void Query_collection_for_detached_throws(QueryTrackingBehavior q context.Entry(left).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(EntityOne)), - Assert.Throws(() => collectionEntry.Query()).Message); + var query = collectionEntry.Query(); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs index eedbc06e86a..ba5ef794c54 100644 --- a/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/ManyToManyLoadTestBase.cs @@ -17,90 +17,116 @@ protected ManyToManyLoadTestBase(TFixture fixture) [ConditionalTheory] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] public virtual async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) { using var context = Fixture.CreateContext(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); var collectionEntry = context.Entry(left).Collection(e => e.TwoSkip); - context.Entry(left).State = state; + SetState(context, left, state, queryTrackingBehavior); Assert.False(collectionEntry.IsLoaded); - if (ExpectLazyLoading) + if (ExpectLazyLoading + && state == EntityState.Detached + && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) { - Assert.Equal(7, left.TwoSkip.Count); + Assert.Null(left.TwoSkip); } else { - if (async) + if (ExpectLazyLoading) { - await collectionEntry.LoadAsync(); + Assert.Equal(7, left.TwoSkip.Count); } else { - collectionEntry.Load(); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } - } - Assert.True(collectionEntry.IsLoaded); - foreach (var entityTwo in left.TwoSkip) - { - Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkip).IsLoaded); - } + Assert.True(collectionEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkip) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.OneSkip).IsLoaded); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); - Assert.Equal(7, left.TwoSkip.Count); - foreach (var right in left.TwoSkip) - { - Assert.Contains(left, right.OneSkip); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(7, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.Contains(left, right.OneSkip); + } } - Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 7 + 7, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query(EntityState state, bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); var collectionEntry = context.Entry(left).Collection(e => e.TwoSkipShared); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(collectionEntry.IsLoaded); @@ -117,15 +143,18 @@ public virtual async Task Load_collection_using_Query(EntityState state, bool as RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(3, left.TwoSkipShared.Count); - foreach (var right in left.TwoSkipShared) - { - Assert.Contains(left, right.OneSkipShared); - } + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 3 + 3, context.ChangeTracker.Entries().Count()); - Assert.Equal(children, left.TwoSkipShared.ToList()); + if (state != EntityState.Detached) + { + Assert.Equal(3, left.TwoSkipShared.Count); + foreach (var right in left.TwoSkipShared) + { + Assert.Contains(left, right.OneSkipShared); + } - Assert.Equal(1 + 3 + 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(children, left.TwoSkipShared.ToList()); + } } [ConditionalTheory] @@ -199,10 +228,14 @@ public virtual void Attached_collections_are_not_marked_as_loaded(EntityState st [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_already_loaded(EntityState state, bool async) { using var context = Fixture.CreateContext(); @@ -213,7 +246,12 @@ public virtual async Task Load_collection_already_loaded(EntityState state, bool var collectionEntry = context.Entry(left).Collection(e => e.ThreeSkipPayloadFull); - context.Entry(left).State = state; + foreach (var two in left.ThreeSkipPayloadFull) + { + SetState(context, two, state); + } + + SetState(context, left, state); Assert.True(collectionEntry.IsLoaded); @@ -248,16 +286,20 @@ public virtual async Task Load_collection_already_loaded(EntityState state, bool Assert.Contains(left, right.OneSkipPayloadFull); } - Assert.Equal(1 + 4 + 4, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 4 + 4, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_already_loaded(EntityState state, bool async) { using var context = Fixture.CreateContext(); @@ -268,7 +310,12 @@ public virtual async Task Load_collection_using_Query_already_loaded(EntityState var collectionEntry = context.Entry(left).Collection(e => e.TwoSkip); - context.Entry(left).State = state; + foreach (var two in left.TwoSkip) + { + SetState(context, two, state); + } + + SetState(context, left, state); Assert.True(collectionEntry.IsLoaded); @@ -291,84 +338,106 @@ public virtual async Task Load_collection_using_Query_already_loaded(EntityState Assert.Contains(left, right.OneSkip); } - Assert.Equal(children, left.TwoSkip.ToList()); + if (state == EntityState.Detached) + { + Assert.NotEqual(children, left.TwoSkip.ToList()); + } + else + { + Assert.Equal(children, left.TwoSkip.ToList()); + } - Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 7 + 7, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_untyped(EntityState state, bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); var navigationEntry = context.Entry(left).Navigation("TwoSkip"); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(navigationEntry.IsLoaded); - if (ExpectLazyLoading) + if (ExpectLazyLoading && state == EntityState.Detached) { - Assert.Equal(7, left.TwoSkip.Count); + Assert.Null(left.TwoSkip); } else { - if (async) + if (ExpectLazyLoading) { - await navigationEntry.LoadAsync(); + Assert.Equal(7, left.TwoSkip.Count); } else { - navigationEntry.Load(); + if (async) + { + await navigationEntry.LoadAsync(); + } + else + { + navigationEntry.Load(); + } } - } - Assert.True(navigationEntry.IsLoaded); - foreach (var entityTwo in left.TwoSkip) - { - Assert.False(context.Entry((object)entityTwo).Collection("OneSkip").IsLoaded); - } + Assert.True(navigationEntry.IsLoaded); + foreach (var entityTwo in left.TwoSkip) + { + Assert.False(context.Entry((object)entityTwo).Collection("OneSkip").IsLoaded); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(7, left.TwoSkip.Count); - foreach (var right in left.TwoSkip) - { - Assert.Contains(left, right.OneSkip); + Assert.Equal(7, left.TwoSkip.Count); + foreach (var right in left.TwoSkip) + { + Assert.Contains(left, right.OneSkip); + } } - Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 7 + 7, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_untyped(EntityState state, bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); var collectionEntry = context.Entry(left).Navigation("TwoSkipShared"); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(collectionEntry.IsLoaded); @@ -385,24 +454,31 @@ public virtual async Task Load_collection_using_Query_untyped(EntityState state, RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(3, left.TwoSkipShared.Count); - foreach (var right in left.TwoSkipShared) - { - Assert.Contains(left, right.OneSkipShared); - } + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 3 + 3, context.ChangeTracker.Entries().Count()); - Assert.Equal(children, left.TwoSkipShared.ToList()); + if (state != EntityState.Detached) + { + Assert.Equal(3, left.TwoSkipShared.Count); + foreach (var right in left.TwoSkipShared) + { + Assert.Contains(left, right.OneSkipShared); + } - Assert.Equal(1 + 3 + 3, context.ChangeTracker.Entries().Count()); + Assert.Equal(children, left.TwoSkipShared.ToList()); + } } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_not_found_untyped(EntityState state, bool async) { using var context = Fixture.CreateContext(); @@ -416,42 +492,54 @@ public virtual async Task Load_collection_not_found_untyped(EntityState state, b var navigationEntry = context.Entry(left).Navigation("TwoSkip"); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(navigationEntry.IsLoaded); - if (ExpectLazyLoading) + if (ExpectLazyLoading && state == EntityState.Detached) { - Assert.Equal(0, left.TwoSkip.Count); + Assert.Null(left.TwoSkip); } else { - if (async) + if (ExpectLazyLoading) { - await navigationEntry.LoadAsync(); + Assert.Equal(0, left.TwoSkip.Count); } else { - navigationEntry.Load(); + if (async) + { + await navigationEntry.LoadAsync(); + } + else + { + navigationEntry.Load(); + } } - } - Assert.True(navigationEntry.IsLoaded); + Assert.True(navigationEntry.IsLoaded); - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Empty(left.TwoSkip); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Empty(left.TwoSkip); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_not_found_untyped(EntityState state, bool async) { using var context = Fixture.CreateContext(); @@ -465,7 +553,7 @@ public virtual async Task Load_collection_using_Query_not_found_untyped(EntitySt var navigationEntry = context.Entry(left).Navigation("TwoSkip"); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(navigationEntry.IsLoaded); @@ -481,22 +569,30 @@ public virtual async Task Load_collection_using_Query_not_found_untyped(EntitySt Assert.Empty(children); Assert.Empty(left.TwoSkip); - Assert.Single(context.ChangeTracker.Entries()); + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, false, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, true, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_collection_already_loaded_untyped(EntityState state, bool async, CascadeTiming deleteOrphansTiming) { using var context = Fixture.CreateContext(); @@ -509,7 +605,12 @@ public virtual async Task Load_collection_already_loaded_untyped(EntityState sta var navigationEntry = context.Entry(left).Navigation("ThreeSkipPayloadFull"); - context.Entry(left).State = state; + foreach (var two in left.ThreeSkipPayloadFull) + { + SetState(context, two, state); + } + + SetState(context, left, state); Assert.True(navigationEntry.IsLoaded); @@ -544,22 +645,30 @@ public virtual async Task Load_collection_already_loaded_untyped(EntityState sta Assert.Contains(left, right.OneSkipPayloadFull); } - Assert.Equal(1 + 4 + 4, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 4 + 4, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Added, false, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, true, CascadeTiming.Immediate)] [InlineData(EntityState.Modified, false, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, true, CascadeTiming.Immediate)] [InlineData(EntityState.Deleted, false, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, true, CascadeTiming.Immediate)] + [InlineData(EntityState.Detached, false, CascadeTiming.Immediate)] [InlineData(EntityState.Unchanged, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Unchanged, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Added, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Modified, false, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, true, CascadeTiming.OnSaveChanges)] [InlineData(EntityState.Deleted, false, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, true, CascadeTiming.OnSaveChanges)] + [InlineData(EntityState.Detached, false, CascadeTiming.OnSaveChanges)] public virtual async Task Load_collection_using_Query_already_loaded_untyped( EntityState state, bool async, @@ -575,7 +684,12 @@ public virtual async Task Load_collection_using_Query_already_loaded_untyped( var navigationEntry = context.Entry(left).Navigation("TwoSkip"); - context.Entry(left).State = state; + foreach (var two in left.TwoSkip) + { + SetState(context, two, state); + } + + SetState(context, left, state); Assert.True(navigationEntry.IsLoaded); @@ -599,84 +713,106 @@ public virtual async Task Load_collection_using_Query_already_loaded_untyped( Assert.Contains(left, right.OneSkip); } - Assert.Equal(children, left.TwoSkip.ToList()); + if (state == EntityState.Detached) + { + Assert.NotEqual(children, left.TwoSkip.ToList()); + } + else + { + Assert.Equal(children, left.TwoSkip.ToList()); + } - Assert.Equal(1 + 7 + 7, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 7 + 7, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_composite_key(EntityState state, bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(7, "7_2", new DateTime(2007, 2, 1)); + var left = context.Set().Find(7, "7_2", new DateTime(2007, 2, 1))!; ClearLog(); var collectionEntry = context.Entry(left).Collection(e => e.ThreeSkipFull); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(collectionEntry.IsLoaded); - if (ExpectLazyLoading) + if (ExpectLazyLoading && state == EntityState.Detached) { - Assert.Equal(2, left.ThreeSkipFull.Count); + Assert.Null(left.ThreeSkipFull); } else { - if (async) + if (ExpectLazyLoading) { - await collectionEntry.LoadAsync(); + Assert.Equal(2, left.ThreeSkipFull.Count); } else { - collectionEntry.Load(); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } - } - Assert.True(collectionEntry.IsLoaded); - foreach (var entityTwo in left.ThreeSkipFull) - { - Assert.False(context.Entry(entityTwo).Collection(e => e.CompositeKeySkipFull).IsLoaded); - } + Assert.True(collectionEntry.IsLoaded); + foreach (var entityTwo in left.ThreeSkipFull) + { + Assert.False(context.Entry(entityTwo).Collection(e => e.CompositeKeySkipFull).IsLoaded); + } - RecordLog(); - context.ChangeTracker.LazyLoadingEnabled = false; + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, left.ThreeSkipFull.Count); - foreach (var right in left.ThreeSkipFull) - { - Assert.Contains(left, right.CompositeKeySkipFull); + Assert.Equal(2, left.ThreeSkipFull.Count); + foreach (var right in left.ThreeSkipFull) + { + Assert.Contains(left, right.CompositeKeySkipFull); + } } - Assert.Equal(1 + 2 + 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 2 + 2, context.ChangeTracker.Entries().Count()); } [ConditionalTheory] [InlineData(EntityState.Unchanged, true)] [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Added, false)] [InlineData(EntityState.Modified, true)] [InlineData(EntityState.Modified, false)] [InlineData(EntityState.Deleted, true)] [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Detached, true)] + [InlineData(EntityState.Detached, false)] public virtual async Task Load_collection_using_Query_composite_key(EntityState state, bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(7, "7_2", new DateTime(2007, 2, 1)); + var left = context.Set().Find(7, "7_2", new DateTime(2007, 2, 1))!; ClearLog(); var collectionEntry = context.Entry(left).Collection(e => e.ThreeSkipFull); - context.Entry(left).State = state; + SetState(context, left, state); Assert.False(collectionEntry.IsLoaded); @@ -693,15 +829,18 @@ public virtual async Task Load_collection_using_Query_composite_key(EntityState RecordLog(); context.ChangeTracker.LazyLoadingEnabled = false; - Assert.Equal(2, left.ThreeSkipFull.Count); - foreach (var right in left.ThreeSkipFull) - { - Assert.Contains(left, right.CompositeKeySkipFull); - } + Assert.Equal(state == EntityState.Detached ? 0 : 1 + 2 + 2, context.ChangeTracker.Entries().Count()); - Assert.Equal(children, left.ThreeSkipFull.ToList()); + if (state != EntityState.Detached) + { + Assert.Equal(2, left.ThreeSkipFull.Count); + foreach (var right in left.ThreeSkipFull) + { + Assert.Contains(left, right.CompositeKeySkipFull); + } - Assert.Equal(1 + 2 + 2, context.ChangeTracker.Entries().Count()); + Assert.Equal(children, left.ThreeSkipFull.ToList()); + } } [ConditionalTheory] @@ -724,20 +863,14 @@ public virtual async Task Load_collection_for_detached_throws(bool async, QueryT context.Entry(left).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(EntityOne)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } [ConditionalTheory] @@ -757,9 +890,7 @@ public virtual void Query_collection_for_detached_throws(QueryTrackingBehavior q context.Entry(left).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(EntityOne)), - Assert.Throws(() => collectionEntry.Query()).Message); + var query = collectionEntry.Query(); } [ConditionalTheory] @@ -769,7 +900,7 @@ public virtual async Task Load_collection_using_Query_with_Include(bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); @@ -818,7 +949,7 @@ public virtual async Task Load_collection_using_Query_with_Include_for_inverse(b { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); @@ -857,7 +988,7 @@ public virtual async Task Load_collection_using_Query_with_Include_for_same_coll { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); @@ -896,7 +1027,7 @@ public virtual async Task Load_collection_using_Query_with_filtered_Include(bool { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); @@ -946,7 +1077,7 @@ public virtual async Task Load_collection_using_Query_with_filtered_Include_and_ { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); @@ -1002,7 +1133,7 @@ public virtual async Task Load_collection_using_Query_with_join(bool async) { using var context = Fixture.CreateContext(); - var left = context.Set().Find(3); + var left = context.Set().Find(3)!; ClearLog(); @@ -1081,6 +1212,18 @@ public virtual async Task Query_with_filtered_Include_marks_only_left_as_loaded( } } + private static void SetState( + DbContext context, + object entity, + EntityState state, + QueryTrackingBehavior queryTrackingBehavior = QueryTrackingBehavior.TrackAll) + { + if (state != (queryTrackingBehavior == QueryTrackingBehavior.TrackAll ? EntityState.Unchanged : EntityState.Detached)) + { + context.Entry(entity).State = state; + } + } + protected virtual void ClearLog() { } diff --git a/test/EFCore.Specification.Tests/UnidirectionalManyToManyLoadTestBase.cs b/test/EFCore.Specification.Tests/UnidirectionalManyToManyLoadTestBase.cs index 0dd7db55055..c71decb216a 100644 --- a/test/EFCore.Specification.Tests/UnidirectionalManyToManyLoadTestBase.cs +++ b/test/EFCore.Specification.Tests/UnidirectionalManyToManyLoadTestBase.cs @@ -31,7 +31,7 @@ public virtual async Task Load_collection_unidirectional(EntityState state, Quer { using var context = Fixture.CreateContext(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; var left = context.Set().Find(3); @@ -721,20 +721,14 @@ public virtual async Task Load_collection_for_detached_throws_unidirectional(boo context.Entry(left).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(UnidirectionalEntityOne)), - (await Assert.ThrowsAsync( - async () => - { - if (async) - { - await collectionEntry.LoadAsync(); - } - else - { - collectionEntry.Load(); - } - })).Message); + if (async) + { + await collectionEntry.LoadAsync(); + } + else + { + collectionEntry.Load(); + } } [ConditionalTheory] @@ -754,9 +748,7 @@ public virtual void Query_collection_for_detached_throws_unidirectional(QueryTra context.Entry(left).State = EntityState.Detached; } - Assert.Equal( - CoreStrings.CannotLoadDetached(nameof(left.TwoSkip), nameof(UnidirectionalEntityOne)), - Assert.Throws(() => collectionEntry.Query()).Message); + var query = collectionEntry.Query(); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/WithConstructorsTestBase.cs b/test/EFCore.Specification.Tests/WithConstructorsTestBase.cs index 7a915cb28ac..bb024439705 100644 --- a/test/EFCore.Specification.Tests/WithConstructorsTestBase.cs +++ b/test/EFCore.Specification.Tests/WithConstructorsTestBase.cs @@ -486,22 +486,17 @@ public virtual void Detaching_entity_resets_lazy_loader_so_it_can_be_reattached( using (var context = CreateContext()) { post = context.Set().OrderBy(e => e.Id).First(); - Assert.NotNull(post.GetLoader()); - context.Entry(post).State = EntityState.Detached; - - Assert.Null(post.GetLoader()); } + Assert.NotNull(post.GetLoader()); Assert.Null(post.LazyPropertyBlog); using (var context = CreateContext()) { context.Attach(post); - Assert.NotNull(post.GetLoader()); - Assert.NotNull(post.LazyPropertyBlog); Assert.Contains(post, post.LazyPropertyBlog.LazyPropertyPosts); } @@ -560,22 +555,17 @@ public virtual void Detaching_entity_resets_lazy_loader_field_so_it_can_be_reatt using (var context = CreateContext()) { post = context.Set().OrderBy(e => e.Id).First(); - Assert.NotNull(post.GetLoader()); - context.Entry(post).State = EntityState.Detached; - - Assert.Null(post.GetLoader()); } + Assert.NotNull(post.GetLoader()); Assert.Null(post.LazyFieldBlog); using (var context = CreateContext()) { context.Attach(post); - Assert.NotNull(post.GetLoader()); - Assert.NotNull(post.LazyFieldBlog); Assert.Contains(post, post.LazyFieldBlog.LazyFieldPosts); } @@ -1665,26 +1655,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity(); modelBuilder.Entity(); modelBuilder.Entity(); - modelBuilder.Entity(); - - // Manually configure service fields since there is no public API yet - - var bindingFactories = context.GetService(); - - var blogServiceProperty = modelBuilder.Entity().Metadata.AddServiceProperty( - typeof(LazyFieldBlog).GetRuntimeFields().Single(f => f.Name == "_loader")); - - blogServiceProperty.ParameterBinding = - (ServiceParameterBinding)bindingFactories.FindFactory(typeof(ILazyLoader), "_loader") - .Bind(blogServiceProperty.DeclaringEntityType, typeof(ILazyLoader), "_loader"); - - var postServiceProperty = modelBuilder.Entity().Metadata.AddServiceProperty( - typeof(LazyFieldPost).GetRuntimeFields().Single(f => f.Name == "_loader")); - - postServiceProperty.ParameterBinding = - (ServiceParameterBinding)bindingFactories.FindFactory(typeof(ILazyLoader), "_loader") - .Bind(postServiceProperty.DeclaringEntityType, typeof(ILazyLoader), "_loader"); + modelBuilder.Entity(); + modelBuilder.Entity(); } protected override void Seed(WithConstructorsContext context) diff --git a/test/EFCore.Specification.Tests/test/EFCore.Specification.Tests/LazyLoadTestBase.cs b/test/EFCore.Specification.Tests/test/EFCore.Specification.Tests/LazyLoadTestBase.cs new file mode 100644 index 00000000000..a34b2c3f2f1 --- /dev/null +++ b/test/EFCore.Specification.Tests/test/EFCore.Specification.Tests/LazyLoadTestBase.cs @@ -0,0 +1,5425 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract partial class LoadTestBase +{ + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await child.LazyLoadParent(async)); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(await child.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await child.LazyLoadParent(async)); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.Children.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await single.LazyLoadParent(async)); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(await single.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await single.LazyLoadParent(async)); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.Single); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_dependent( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadSingle(async)); // Explicitly detached + } + else + { + Assert.NotNull(await parent.LazyLoadSingle(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityState state, QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(single.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.SinglePkToPk); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SinglePkToPk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityState state, QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SinglePkToPk); // Explicitly detached + } + else + { + Assert.NotNull(parent.SinglePkToPk); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(parent, parent.SinglePkToPk.Parent); + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_null_FK( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new Child { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await child.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_null_FK( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new Single { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await single.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection_not_found(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.Empty(await parent.LazyLoadChildren(async)); + Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_not_found( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new Child { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await child.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_not_found( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new Single { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await single.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_dependent_not_found( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new Parent { Id = 767, AlternateId = "NewRoot" }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await parent.LazyLoadSingle(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Null(parent.Single); + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection_already_loaded( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Children).Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.True(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await parent.LazyLoadChildren(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll + && state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); + } + else + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection_already_partially_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + context.ChangeTracker.LazyLoadingEnabled = false; + + var child = context.Set().OrderBy(e => e.Id).First(); + var parent = context.Set().Single(); + if (parent.Children == null) + { + parent.Children = new List { child }; + child.Parent = parent; + } + + context.ChangeTracker.LazyLoadingEnabled = true; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await parent.LazyLoadChildren(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + RecordLog(); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(collectionEntry.IsLoaded); // Explicitly detached + Assert.Equal(1, parent.Children.Count()); + + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + else + { + Assert.True(collectionEntry.IsLoaded); + + context.ChangeTracker.LazyLoadingEnabled = false; + + // Note that when detached there is no identity resolution, so loading results in duplicates + Assert.Equal(state == EntityState.Detached ? 3 : 2, parent.Children.Count()); + + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Include(e => e.Parent).Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child.Parent, state, queryTrackingBehavior); + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(await child.LazyLoadParent(async)); + } + else + { + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await child.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(child, child.Parent.Children.Single()); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Include(e => e.Parent).Single(); + + ClearLog(); + + SetState(context, single.Parent, state, queryTrackingBehavior); + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(await single.LazyLoadParent(async)); + } + else + { + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await single.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(single, single.Parent.Single); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_dependent_already_loaded( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Single).Single(); + + ClearLog(); + + SetState(context, parent.Single, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await parent.LazyLoadSingle(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Include(e => e.Parent).Single(); + + ClearLog(); + + SetState(context, single.Parent, state, queryTrackingBehavior); + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(single, single.Parent.SinglePkToPk); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SinglePkToPk); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.SinglePkToPk).Single(); + + ClearLog(); + + SetState(context, parent.SinglePkToPk, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.SinglePkToPk); + + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.SinglePkToPk); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(parent, parent.SinglePkToPk.Parent); + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SinglePkToPk); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var child = context.Set().Single(e => e.Id == 32); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(child.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.ChildrenAk.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenAk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenAk.Single()); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(single.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.SingleAk); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleAk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleAk); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.SingleAk); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SingleAk); // Explicitly detached + } + else + { + Assert.NotNull(parent.SingleAk); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(parent, parent.SingleAk.Parent); + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SingleAk); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var child = context.Attach(new ChildAk { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + Assert.Null(child.Parent); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var single = context.Attach(new SingleAk { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + Assert.Null(single.Parent); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_shadow_fk(EntityState state, QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.ChildrenShadowFk); + + Assert.False(collectionEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.ChildrenShadowFk); // Explicitly detached + } + else + { + Assert.NotNull(parent.ChildrenShadowFk); + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.ChildrenShadowFk.Count()); + Assert.All(parent.ChildrenShadowFk.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var child = context.Set().Single(e => e.Id == 52); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(child.Parent); // Explicitly detached + } + else if (state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => child.Parent).Message); + } + else + { + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.ChildrenShadowFk.Single()); + } + + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenShadowFk); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenShadowFk.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(single.Parent); // Explicitly detached + } + else if (state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + Assert.Throws(() => single.Parent).Message); + } + else + { + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.SingleShadowFk); + } + + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleShadowFk); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleShadowFk); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.SingleShadowFk); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SingleShadowFk); // Explicitly detached + } + else + { + Assert.NotNull(parent.SingleShadowFk); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(parent, parent.SingleShadowFk.Parent); + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SingleShadowFk); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var child = context.Attach(new ChildShadowFk { Id = 767 }).Entity; + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + context.Entry(child).Property("ParentId").CurrentValue = null; + } + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + if (state == EntityState.Detached) + { + Assert.Null(child.Parent); // Explicitly detached + } + else if (queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "ChildShadowFk"), + Assert.Throws(() => child.Parent).Message); + } + else + { + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + Assert.Null(child.Parent); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + Assert.Null(child.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var single = context.Attach(new SingleShadowFk { Id = 767 }).Entity; + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + context.Entry(single).Property("ParentId").CurrentValue = null; + } + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + if (state == EntityState.Detached) + { + Assert.Null(single.Parent); + } + else if (queryTrackingBehavior != QueryTrackingBehavior.TrackAll) + { + Assert.Equal( + CoreStrings.CannotLoadDetachedShadow("Parent", "SingleShadowFk"), + Assert.Throws(() => single.Parent).Message); + } + else + { + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + Assert.Null(single.Parent); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + + Assert.Null(single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_composite_key(EntityState state, QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.ChildrenCompositeKey); + + Assert.False(collectionEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.ChildrenCompositeKey); // Explicitly detached + } + else + { + Assert.NotNull(parent.ChildrenCompositeKey); + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.ChildrenCompositeKey.Count()); + Assert.All(parent.ChildrenCompositeKey.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var child = context.Set().Single(e => e.Id == 52); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(child.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.ChildrenCompositeKey.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.ChildrenCompositeKey); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.ChildrenCompositeKey.Single()); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(single.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.SingleCompositeKey); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.SingleCompositeKey); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.SingleCompositeKey); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.SingleCompositeKey); + + Assert.False(referenceEntry.IsLoaded); + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.SingleCompositeKey); // Explicitly detached + } + else + { + Assert.NotNull(parent.SingleCompositeKey); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(parent, parent.SingleCompositeKey.Parent); + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.SingleCompositeKey); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var child = context.Attach(new ChildCompositeKey { Id = 767, ParentId = 567 }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + Assert.Null(child.Parent); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var single = context.Attach(new SingleCompositeKey { Id = 767, ParentAlternateId = "Boot" }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + Assert.Null(single.Parent); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.NotNull(await parent.LazyLoadChildren(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await child.LazyLoadParent(async)); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(await child.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await child.LazyLoadParent(async)); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.Children.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await single.LazyLoadParent(async)); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(await single.LazyLoadParent(async)); + } + else + { + Assert.NotNull(await single.LazyLoadParent(async)); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.Single); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_dependent_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(await parent.LazyLoadSingle(async)); // Explicitly detached + } + else + { + Assert.NotNull(await parent.LazyLoadSingle(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_null_FK_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildFullLoaderByConstructor { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await child.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_null_FK_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleFullLoaderByConstructor { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await single.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection_not_found_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentFullLoaderByConstructor { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached) + { + Assert.Null(await parent.LazyLoadChildren(async)); // Explicitly detached + } + else + { + Assert.Empty(await parent.LazyLoadChildren(async)); + Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_not_found_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildFullLoaderByConstructor { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await child.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_not_found_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleFullLoaderByConstructor { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await single.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_dependent_not_found_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentFullLoaderByConstructor { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(await parent.LazyLoadSingle(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Null(parent.Single); + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_collection_already_loaded_full_loader_constructor_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Children).Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.True(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await parent.LazyLoadChildren(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll + && state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); + } + else + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_many_to_one_reference_to_principal_already_loaded_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Include(e => e.Parent).Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child.Parent, state, queryTrackingBehavior); + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(await child.LazyLoadParent(async)); + } + else + { + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await child.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(child, child.Parent.Children.Single()); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + public virtual async Task Lazy_load_one_to_one_reference_to_principal_already_loaded_full_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Include(e => e.Parent).Single(); + + ClearLog(); + + SetState(context, single.Parent, state, queryTrackingBehavior); + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(await single.LazyLoadParent(async)); + } + else + { + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await single.LazyLoadParent(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(single, single.Parent.Single); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, true)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution, false)] + public virtual async Task Lazy_load_one_to_one_reference_to_dependent_already_loaded_full_loader_constructor_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior, + bool async) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Single).Single(); + + ClearLog(); + + SetState(context, parent.Single, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.True(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(await parent.LazyLoadSingle(async)); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.Children.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.Single); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildDelegateLoaderByConstructor { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.False(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleDelegateLoaderByConstructor { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.False(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_not_found_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentDelegateLoaderByConstructor { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + // Delegate not set because delegate constructor not called + Assert.Null(parent.Children); + Assert.False(changeDetector.DetectChangesCalled); + Assert.False(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_not_found_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildDelegateLoaderByConstructor { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.False(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_not_found_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleDelegateLoaderByConstructor { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.False(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentDelegateLoaderByConstructor { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.False(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Null(parent.Single); + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_already_loaded_delegate_loader_constructor_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Children).Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + // Loader delegate has no way of recording loader state for untracked queries or detached entities + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll + && state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); + } + else + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Include(e => e.Parent).Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child.Parent, state, queryTrackingBehavior); + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(child.Parent); + } + else + { + // Delegate loader cannot influence IsLoader flag + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + // Delegate loader cannot influence IsLoader flag + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(child, child.Parent.Children.Single()); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded_delegate_loader_constructor_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Include(e => e.Parent).Single(); + + ClearLog(); + + SetState(context, single.Parent, state, queryTrackingBehavior); + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + } + else + { + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(single, single.Parent.Single); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded_delegate_loader_constructor_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Single).Single(); + + ClearLog(); + + SetState(context, parent.Single, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Children); // Explicitly detached + } + else + { + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(collectionEntry.IsLoaded); + + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + } + + Assert.Equal(state == EntityState.Detached ? 0 : 3, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(child.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + } + else + { + Assert.NotNull(child.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(child, child.Parent!.Children.Single()); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(child.Parent); + Assert.Null(parent.Children); + } + else + { + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Single(); + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(single.Parent); // Explicitly detached + } + else + { + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + } + else + { + Assert.NotNull(single.Parent); + } + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(single, single.Parent!.Single); + } + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + if (state == EntityState.Deleted) + { + Assert.Null(single.Parent); + Assert.Null(parent.Single); + } + else + { + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.Null(parent.Single); // Explicitly detached + } + else + { + Assert.NotNull(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.True(referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state != EntityState.Deleted) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_null_FK_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildDelegateLoaderByProperty { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_null_FK_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleDelegateLoaderByProperty { Id = 767, ParentId = null }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_not_found_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentDelegateLoaderByProperty { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + Assert.False(collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + if (state == EntityState.Detached) + { + Assert.Null(parent.Children); // Explicitly detached + } + else + { + Assert.Empty(parent.Children); + Assert.False(changeDetector.DetectChangesCalled); + Assert.True(collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Single(context.ChangeTracker.Entries()); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_not_found_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Attach(new ChildDelegateLoaderByProperty { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, child, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + Assert.Null(child.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_not_found_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Attach(new SingleDelegateLoaderByProperty { Id = 767, ParentId = 787 }).Entity; + + ClearLog(); + + SetState(context, single, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + + Assert.Null(single.Parent); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_not_found_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Attach(new ParentDelegateLoaderByProperty { Id = 767 }).Entity; + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior, isAttached: true); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.False(referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.Null(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Null(parent.Single); + + Assert.Equal(state == EntityState.Detached ? 0 : 1, context.ChangeTracker.Entries().Count()); + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_collection_already_loaded_delegate_loader_property_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Children).Single(); + + ClearLog(); + + SetState(context, parent, state, queryTrackingBehavior); + + var collectionEntry = context.Entry(parent).Collection(e => e.Children); + + // Loader delegate has no way of recording loader state for untracked queries or detached entities + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, collectionEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Children); + + Assert.False(changeDetector.DetectChangesCalled); + + // Loader delegate has no way of recording loader state for untracked queries or detached entities + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, collectionEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(2, parent.Children.Count()); + + if (queryTrackingBehavior == QueryTrackingBehavior.TrackAll + && state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Null(c)); + } + else + { + Assert.All(parent.Children.Select(e => e.Parent), c => Assert.Same(parent, c)); + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_many_to_one_reference_to_principal_already_loaded_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var child = context.Set().Include(e => e.Parent).Single(e => e.Id == 12); + + ClearLog(); + + SetState(context, child.Parent, state, queryTrackingBehavior); + SetState(context, child, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(child).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(child.Parent); + } + else + { + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(child.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(child, child.Parent.Children.Single()); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, child.Parent); + Assert.Same(child, parent.Children.Single()); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_principal_already_loaded_delegate_loader_property_injection( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var single = context.Set().Include(e => e.Parent).Single(); + + ClearLog(); + + SetState(context, single.Parent, state, queryTrackingBehavior); + SetState(context, single, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(single).Reference(e => e.Parent); + + if (state == EntityState.Deleted && queryTrackingBehavior == QueryTrackingBehavior.TrackAll) + { + Assert.False(referenceEntry.IsLoaded); + Assert.Null(single.Parent); + } + else + { + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(single.Parent); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + Assert.Same(single, single.Parent.Single); + + if (state != EntityState.Detached) + { + var parent = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(parent, single.Parent); + Assert.Same(single, parent.Single); + } + } + } + + [ConditionalTheory] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.TrackAll)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTracking)] + [InlineData(EntityState.Unchanged, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.Immediate, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Unchanged, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Added, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Modified, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Deleted, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(EntityState.Detached, CascadeTiming.OnSaveChanges, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public virtual void Lazy_load_one_to_one_reference_to_dependent_already_loaded_delegate_loader_property_injection( + EntityState state, + CascadeTiming deleteOrphansTiming, + QueryTrackingBehavior queryTrackingBehavior) + { + using var context = CreateContext(lazyLoadingEnabled: true); + context.ChangeTracker.QueryTrackingBehavior = queryTrackingBehavior; + context.ChangeTracker.DeleteOrphansTiming = deleteOrphansTiming; + + var changeDetector = (ChangeDetectorProxy)context.GetService(); + + var parent = context.Set().Include(e => e.Single).Single(); + + ClearLog(); + + SetState(context, parent.Single, state, queryTrackingBehavior); + SetState(context, parent, state, queryTrackingBehavior); + + var referenceEntry = context.Entry(parent).Reference(e => e.Single); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + changeDetector.DetectChangesCalled = false; + + Assert.NotNull(parent.Single); + + Assert.False(changeDetector.DetectChangesCalled); + + Assert.Equal(queryTrackingBehavior == QueryTrackingBehavior.TrackAll && state != EntityState.Detached, referenceEntry.IsLoaded); + + RecordLog(); + context.ChangeTracker.LazyLoadingEnabled = false; + + Assert.Equal(state == EntityState.Detached ? 0 : 2, context.ChangeTracker.Entries().Count()); + + if (state == EntityState.Deleted + && deleteOrphansTiming != CascadeTiming.Never) + { + Assert.Same(parent, parent.Single.Parent); + } + + if (state != EntityState.Detached) + { + var single = context.ChangeTracker.Entries().Single().Entity; + + Assert.Same(single, parent.Single); + Assert.Same(parent, single.Parent); + } + } + + [ConditionalFact] + public virtual void Lazy_loading_uses_field_access_when_abstract_base_class_navigation() + { + using var context = CreateContext(lazyLoadingEnabled: true); + var product = context.Set().Single(); + var deposit = product.Deposit; + + Assert.NotNull(deposit); + Assert.Same(deposit, product.Deposit); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/LazyLoadProxySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/LazyLoadProxySqlServerTest.cs index 57257ccb1dc..2f612d96525 100644 --- a/test/EFCore.SqlServer.FunctionalTests/LazyLoadProxySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/LazyLoadProxySqlServerTest.cs @@ -16,7 +16,9 @@ public override void Lazy_load_collection(EntityState state, bool useAttach, boo base.Lazy_load_collection(state, useAttach, useDetach); AssertSql( -""" + state == EntityState.Detached && useAttach + ? "" + : """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -30,10 +32,12 @@ public override void Lazy_load_many_to_one_reference_to_principal(EntityState st base.Lazy_load_many_to_one_reference_to_principal(state, useAttach, useDetach); AssertSql( -""" + state == EntityState.Detached && useAttach + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -44,10 +48,12 @@ public override void Lazy_load_one_to_one_reference_to_principal(EntityState sta base.Lazy_load_one_to_one_reference_to_principal(state, useAttach, useDetach); AssertSql( -""" + state == EntityState.Detached && useAttach + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -58,10 +64,12 @@ public override void Lazy_load_one_to_one_reference_to_dependent(EntityState sta base.Lazy_load_one_to_one_reference_to_dependent(state, useAttach, useDetach); AssertSql( -""" + state == EntityState.Detached && useAttach + ? "" + : """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -72,10 +80,10 @@ public override void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(Entity base.Lazy_load_one_to_one_PK_to_PK_reference_to_principal(state); AssertSql( -""" + """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -86,10 +94,10 @@ public override void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(Entity base.Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(state); AssertSql( -""" + """ @__p_0='707' -SELECT [s].[Id] +SELECT TOP(1) [s].[Id] FROM [SinglePkToPk] AS [s] WHERE [s].[Id] = @__p_0 """); @@ -114,7 +122,7 @@ public override void Lazy_load_collection_not_found(EntityState state) base.Lazy_load_collection_not_found(state); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -128,10 +136,10 @@ public override void Lazy_load_many_to_one_reference_to_principal_not_found(Enti base.Lazy_load_many_to_one_reference_to_principal_not_found(state); AssertSql( -""" + """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -142,10 +150,10 @@ public override void Lazy_load_one_to_one_reference_to_principal_not_found(Entit base.Lazy_load_one_to_one_reference_to_principal_not_found(state); AssertSql( -""" + """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -156,10 +164,10 @@ public override void Lazy_load_one_to_one_reference_to_dependent_not_found(Entit base.Lazy_load_one_to_one_reference_to_dependent_not_found(state); AssertSql( -""" + """ @__p_0='767' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -216,10 +224,10 @@ public override void Lazy_load_many_to_one_reference_to_principal_alternate_key( base.Lazy_load_many_to_one_reference_to_principal_alternate_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 """); @@ -230,10 +238,10 @@ public override void Lazy_load_one_to_one_reference_to_principal_alternate_key(E base.Lazy_load_one_to_one_reference_to_principal_alternate_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 """); @@ -244,10 +252,10 @@ public override void Lazy_load_one_to_one_reference_to_dependent_alternate_key(E base.Lazy_load_one_to_one_reference_to_dependent_alternate_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [SingleAk] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -272,7 +280,7 @@ public override void Lazy_load_collection_shadow_fk(EntityState state) base.Lazy_load_collection_shadow_fk(state); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -286,10 +294,12 @@ public override void Lazy_load_many_to_one_reference_to_principal_shadow_fk(Enti base.Lazy_load_many_to_one_reference_to_principal_shadow_fk(state); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -300,10 +310,12 @@ public override void Lazy_load_one_to_one_reference_to_principal_shadow_fk(Entit base.Lazy_load_one_to_one_reference_to_principal_shadow_fk(state); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -314,10 +326,10 @@ public override void Lazy_load_one_to_one_reference_to_dependent_shadow_fk(Entit base.Lazy_load_one_to_one_reference_to_dependent_shadow_fk(state); AssertSql( -""" + """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [SingleShadowFk] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -342,7 +354,7 @@ public override void Lazy_load_collection_composite_key(EntityState state) base.Lazy_load_collection_composite_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) @@ -357,11 +369,11 @@ public override void Lazy_load_many_to_one_reference_to_principal_composite_key( base.Lazy_load_many_to_one_reference_to_principal_composite_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 AND [p].[Id] = @__p_1 """); @@ -372,11 +384,11 @@ public override void Lazy_load_one_to_one_reference_to_principal_composite_key(E base.Lazy_load_one_to_one_reference_to_principal_composite_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' -SELECT [p].[Id], [p].[AlternateId], [p].[Discriminator] +SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 AND [p].[Id] = @__p_1 """); @@ -387,11 +399,11 @@ public override void Lazy_load_one_to_one_reference_to_dependent_composite_key(E base.Lazy_load_one_to_one_reference_to_dependent_composite_key(state); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentAlternateId], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentAlternateId], [s].[ParentId] FROM [SingleCompositeKey] AS [s] WHERE [s].[ParentAlternateId] = @__p_0 AND [s].[ParentId] = @__p_1 """); @@ -418,7 +430,7 @@ public override async Task Load_collection(EntityState state, bool async) if (!async) { AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -434,14 +446,14 @@ public override void Top_level_projection_track_entities_before_passing_to_clien base.Top_level_projection_track_entities_before_passing_to_client_method(); AssertSql( -""" + """ SELECT TOP(1) [p].[Id], [p].[AlternateId], [p].[Discriminator] FROM [Parent] AS [p] ORDER BY [p].[Id] @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -452,7 +464,7 @@ public override async Task Entity_equality_with_proxy_parameter(bool async) await base.Entity_equality_with_proxy_parameter(async); AssertSql( -""" + """ @__entity_equality_called_0_Id='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -473,10 +485,12 @@ protected override void RecordLog() private void AssertSql(string expected) { + expected ??= ""; + var sql = Sql ?? ""; try { Assert.Equal( - expected, Sql, ignoreLineEndingDifferences: true); + expected, sql, ignoreLineEndingDifferences: true); } catch { @@ -501,7 +515,7 @@ private void AssertSql(string expected) var testInfo = testName + " : " + lineNumber + FileNewLine; var newBaseLine = $@" AssertSql( - {"@\"" + Sql.Replace("\"", "\"\"") + "\""}); + {"@\"" + sql.Replace("\"", "\"\"") + "\""}); "; diff --git a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs index 4cc9aade93f..117a001c1d4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/LoadSqlServerTest.cs @@ -11,12 +11,14 @@ public LoadSqlServerTest(LoadSqlServerFixture fixture) fixture.TestSqlLoggerFactory.Clear(); } - public override void Lazy_load_collection(EntityState state) + public override async Task Lazy_load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) { - base.Lazy_load_collection(state); + await base.Lazy_load_collection(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -25,96 +27,127 @@ FROM [Child] AS [c] """); } - public override void Lazy_load_many_to_one_reference_to_principal(EntityState state) + public override async Task Lazy_load_many_to_one_reference_to_principal( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_many_to_one_reference_to_principal(state); + await base.Lazy_load_many_to_one_reference_to_principal(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_principal(EntityState state) + public override async Task Lazy_load_one_to_one_reference_to_principal( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_principal(state); + await base.Lazy_load_one_to_one_reference_to_principal(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_dependent(EntityState state) + public override async Task Lazy_load_one_to_one_reference_to_dependent( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_dependent(state); + await base.Lazy_load_one_to_one_reference_to_dependent(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); } - public override void Lazy_load_one_to_one_PK_to_PK_reference_to_principal(EntityState state) + public override void Lazy_load_one_to_one_PK_to_PK_reference_to_principal( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_PK_to_PK_reference_to_principal(state); + base.Lazy_load_one_to_one_PK_to_PK_reference_to_principal(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(EntityState state) + public override void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(state); + base.Lazy_load_one_to_one_PK_to_PK_reference_to_dependent(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' -SELECT [s].[Id] +SELECT TOP(1) [s].[Id] FROM [SinglePkToPk] AS [s] WHERE [s].[Id] = @__p_0 """); } - public override void Lazy_load_many_to_one_reference_to_principal_null_FK(EntityState state) + public override async Task Lazy_load_many_to_one_reference_to_principal_null_FK( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_many_to_one_reference_to_principal_null_FK(state); + await base.Lazy_load_many_to_one_reference_to_principal_null_FK(state, queryTrackingBehavior, async); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_reference_to_principal_null_FK(EntityState state) + public override async Task Lazy_load_one_to_one_reference_to_principal_null_FK( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_principal_null_FK(state); + await base.Lazy_load_one_to_one_reference_to_principal_null_FK(state, queryTrackingBehavior, async); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_collection_not_found(EntityState state) + public override async Task Lazy_load_collection_not_found(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) { - base.Lazy_load_collection_not_found(state); + await base.Lazy_load_collection_not_found(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='767' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -123,154 +156,203 @@ FROM [Child] AS [c] """); } - public override void Lazy_load_many_to_one_reference_to_principal_not_found(EntityState state) + public override async Task Lazy_load_many_to_one_reference_to_principal_not_found( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_many_to_one_reference_to_principal_not_found(state); + await base.Lazy_load_many_to_one_reference_to_principal_not_found(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_principal_not_found(EntityState state) + public override async Task Lazy_load_one_to_one_reference_to_principal_not_found( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_principal_not_found(state); + await base.Lazy_load_one_to_one_reference_to_principal_not_found(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_dependent_not_found(EntityState state) + public override async Task Lazy_load_one_to_one_reference_to_dependent_not_found( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_dependent_not_found(state); + await base.Lazy_load_one_to_one_reference_to_dependent_not_found(state, queryTrackingBehavior, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='767' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); } - public override void Lazy_load_collection_already_loaded(EntityState state, CascadeTiming cascadeDeleteTiming) + public override async Task Lazy_load_collection_already_loaded( + EntityState state, + CascadeTiming cascadeDeleteTiming, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_collection_already_loaded(state, cascadeDeleteTiming); + await base.Lazy_load_collection_already_loaded(state, cascadeDeleteTiming, queryTrackingBehavior, async); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_many_to_one_reference_to_principal_already_loaded(EntityState state) + public override async Task Lazy_load_many_to_one_reference_to_principal_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_many_to_one_reference_to_principal_already_loaded(state); + await base.Lazy_load_many_to_one_reference_to_principal_already_loaded(state, queryTrackingBehavior, async); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_reference_to_principal_already_loaded(EntityState state) + public override async Task Lazy_load_one_to_one_reference_to_principal_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_principal_already_loaded(state); + await base.Lazy_load_one_to_one_reference_to_principal_already_loaded(state, queryTrackingBehavior, async); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_reference_to_dependent_already_loaded( + public override async Task Lazy_load_one_to_one_reference_to_dependent_already_loaded( EntityState state, - CascadeTiming cascadeDeleteTiming) + CascadeTiming cascadeDeleteTiming, + QueryTrackingBehavior queryTrackingBehavior, + bool async) { - base.Lazy_load_one_to_one_reference_to_dependent_already_loaded(state, cascadeDeleteTiming); + await base.Lazy_load_one_to_one_reference_to_dependent_already_loaded(state, cascadeDeleteTiming, queryTrackingBehavior, async); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(EntityState state) + public override void Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(state); + base.Lazy_load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(EntityState state) + public override void Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(state); + base.Lazy_load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_many_to_one_reference_to_principal_alternate_key(EntityState state) + public override void Lazy_load_many_to_one_reference_to_principal_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_many_to_one_reference_to_principal_alternate_key(state); + base.Lazy_load_many_to_one_reference_to_principal_alternate_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_principal_alternate_key(EntityState state) + public override void Lazy_load_one_to_one_reference_to_principal_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_principal_alternate_key(state); + base.Lazy_load_one_to_one_reference_to_principal_alternate_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_dependent_alternate_key(EntityState state) + public override void Lazy_load_one_to_one_reference_to_dependent_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_dependent_alternate_key(state); + base.Lazy_load_one_to_one_reference_to_dependent_alternate_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [SingleAk] AS [s] WHERE [s].[ParentId] = @__p_0 """); } - public override void Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key(EntityState state) + public override void Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key(state); + base.Lazy_load_many_to_one_reference_to_principal_null_FK_alternate_key(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key(EntityState state) + public override void Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key(state); + base.Lazy_load_one_to_one_reference_to_principal_null_FK_alternate_key(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_collection_shadow_fk(EntityState state) + public override void Lazy_load_collection_shadow_fk(EntityState state, QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_collection_shadow_fk(state); + base.Lazy_load_collection_shadow_fk(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -279,68 +361,86 @@ FROM [ChildShadowFk] AS [c] """); } - public override void Lazy_load_many_to_one_reference_to_principal_shadow_fk(EntityState state) + public override void Lazy_load_many_to_one_reference_to_principal_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_many_to_one_reference_to_principal_shadow_fk(state); + base.Lazy_load_many_to_one_reference_to_principal_shadow_fk(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_principal_shadow_fk(EntityState state) + public override void Lazy_load_one_to_one_reference_to_principal_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_principal_shadow_fk(state); + base.Lazy_load_one_to_one_reference_to_principal_shadow_fk(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached || queryTrackingBehavior != QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); } - public override void Lazy_load_one_to_one_reference_to_dependent_shadow_fk(EntityState state) + public override void Lazy_load_one_to_one_reference_to_dependent_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_dependent_shadow_fk(state); + base.Lazy_load_one_to_one_reference_to_dependent_shadow_fk(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [SingleShadowFk] AS [s] WHERE [s].[ParentId] = @__p_0 """); } - public override void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state) + public override void Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk(state); + base.Lazy_load_many_to_one_reference_to_principal_null_FK_shadow_fk(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state) + public override void Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk(state); + base.Lazy_load_one_to_one_reference_to_principal_null_FK_shadow_fk(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_collection_composite_key(EntityState state) + public override void Lazy_load_collection_composite_key(EntityState state, QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_collection_composite_key(state); + base.Lazy_load_collection_composite_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) @@ -350,63 +450,79 @@ FROM [ChildCompositeKey] AS [c] """); } - public override void Lazy_load_many_to_one_reference_to_principal_composite_key(EntityState state) + public override void Lazy_load_many_to_one_reference_to_principal_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_many_to_one_reference_to_principal_composite_key(state); + base.Lazy_load_many_to_one_reference_to_principal_composite_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) @__p_1='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 AND [p].[Id] = @__p_1 """); } - public override void Lazy_load_one_to_one_reference_to_principal_composite_key(EntityState state) + public override void Lazy_load_one_to_one_reference_to_principal_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_principal_composite_key(state); + base.Lazy_load_one_to_one_reference_to_principal_composite_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) @__p_1='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 AND [p].[Id] = @__p_1 """); } - public override void Lazy_load_one_to_one_reference_to_dependent_composite_key(EntityState state) + public override void Lazy_load_one_to_one_reference_to_dependent_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_dependent_composite_key(state); + base.Lazy_load_one_to_one_reference_to_dependent_composite_key(state, queryTrackingBehavior); AssertSql( -""" + state == EntityState.Detached && queryTrackingBehavior == QueryTrackingBehavior.TrackAll + ? "" + : """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentAlternateId], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentAlternateId], [s].[ParentId] FROM [SingleCompositeKey] AS [s] WHERE [s].[ParentAlternateId] = @__p_0 AND [s].[ParentId] = @__p_1 """); } - public override void Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key(EntityState state) + public override void Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key(state); + base.Lazy_load_many_to_one_reference_to_principal_null_FK_composite_key(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } - public override void Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key(EntityState state) + public override void Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key( + EntityState state, + QueryTrackingBehavior queryTrackingBehavior) { - base.Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key(state); + base.Lazy_load_one_to_one_reference_to_principal_null_FK_composite_key(state, queryTrackingBehavior); - AssertSql(@""); + AssertSql(); } public override async Task Load_collection(EntityState state, QueryTrackingBehavior queryTrackingBehavior, bool async) @@ -414,7 +530,7 @@ public override async Task Load_collection(EntityState state, QueryTrackingBehav await base.Load_collection(state, queryTrackingBehavior, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -428,10 +544,10 @@ public override async Task Load_many_to_one_reference_to_principal(EntityState s await base.Load_many_to_one_reference_to_principal(state, async); AssertSql( -""" + """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -442,10 +558,10 @@ public override async Task Load_one_to_one_reference_to_principal(EntityState st await base.Load_one_to_one_reference_to_principal(state, async); AssertSql( -""" + """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -456,10 +572,10 @@ public override async Task Load_one_to_one_reference_to_dependent(EntityState st await base.Load_one_to_one_reference_to_dependent(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -470,10 +586,10 @@ public override async Task Load_one_to_one_PK_to_PK_reference_to_principal(Entit await base.Load_one_to_one_PK_to_PK_reference_to_principal(state, async); AssertSql( -""" + """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -484,10 +600,10 @@ public override async Task Load_one_to_one_PK_to_PK_reference_to_dependent(Entit await base.Load_one_to_one_PK_to_PK_reference_to_dependent(state, async); AssertSql( -""" + """ @__p_0='707' -SELECT [s].[Id] +SELECT TOP(1) [s].[Id] FROM [SinglePkToPk] AS [s] WHERE [s].[Id] = @__p_0 """); @@ -498,7 +614,7 @@ public override async Task Load_collection_using_Query(EntityState state, bool a await base.Load_collection_using_Query(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -512,7 +628,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query(E await base.Load_many_to_one_reference_to_principal_using_Query(state, async); AssertSql( -""" + """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -526,7 +642,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query(En await base.Load_one_to_one_reference_to_principal_using_Query(state, async); AssertSql( -""" + """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -540,7 +656,9 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query(En await base.Load_one_to_one_reference_to_dependent_using_Query(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' (Nullable = true) SELECT TOP(2) [s].[Id], [s].[ParentId] @@ -554,7 +672,7 @@ public override async Task Load_one_to_one_PK_to_PK_reference_to_principal_using await base.Load_one_to_one_PK_to_PK_reference_to_principal_using_Query(state, async); AssertSql( -""" + """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -568,7 +686,9 @@ public override async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using await base.Load_one_to_one_PK_to_PK_reference_to_dependent_using_Query(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' SELECT TOP(2) [s].[Id] @@ -581,14 +701,14 @@ public override async Task Load_many_to_one_reference_to_principal_null_FK(Entit { await base.Load_many_to_one_reference_to_principal_null_FK(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_principal_null_FK(EntityState state, bool async) { await base.Load_one_to_one_reference_to_principal_null_FK(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_many_to_one_reference_to_principal_using_Query_null_FK(EntityState state, bool async) @@ -596,7 +716,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n await base.Load_many_to_one_reference_to_principal_using_Query_null_FK(state, async); AssertSql( -""" + """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -608,7 +728,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu await base.Load_one_to_one_reference_to_principal_using_Query_null_FK(state, async); AssertSql( -""" + """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -620,7 +740,7 @@ public override async Task Load_collection_not_found(EntityState state, bool asy await base.Load_collection_not_found(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -634,10 +754,10 @@ public override async Task Load_many_to_one_reference_to_principal_not_found(Ent await base.Load_many_to_one_reference_to_principal_not_found(state, async); AssertSql( -""" + """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -648,10 +768,10 @@ public override async Task Load_one_to_one_reference_to_principal_not_found(Enti await base.Load_one_to_one_reference_to_principal_not_found(state, async); AssertSql( -""" + """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -662,10 +782,10 @@ public override async Task Load_one_to_one_reference_to_dependent_not_found(Enti await base.Load_one_to_one_reference_to_dependent_not_found(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -676,7 +796,7 @@ public override async Task Load_collection_using_Query_not_found(EntityState sta await base.Load_collection_using_Query_not_found(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -690,7 +810,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n await base.Load_many_to_one_reference_to_principal_using_Query_not_found(state, async); AssertSql( -""" + """ @__p_0='787' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -704,7 +824,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_no await base.Load_one_to_one_reference_to_principal_using_Query_not_found(state, async); AssertSql( -""" + """ @__p_0='787' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -718,7 +838,7 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_no await base.Load_one_to_one_reference_to_dependent_using_Query_not_found(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT TOP(2) [s].[Id], [s].[ParentId] @@ -731,14 +851,14 @@ public override async Task Load_collection_already_loaded(EntityState state, boo { await base.Load_collection_already_loaded(state, async, cascadeDeleteTiming); - AssertSql(@""); + AssertSql(); } public override async Task Load_many_to_one_reference_to_principal_already_loaded(EntityState state, bool async) { await base.Load_many_to_one_reference_to_principal_already_loaded(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_principal_already_loaded( @@ -748,7 +868,7 @@ public override async Task Load_one_to_one_reference_to_principal_already_loaded { await base.Load_one_to_one_reference_to_principal_already_loaded(state, async, cascadeDeleteTiming); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_dependent_already_loaded( @@ -758,21 +878,21 @@ public override async Task Load_one_to_one_reference_to_dependent_already_loaded { await base.Load_one_to_one_reference_to_dependent_already_loaded(state, async, cascadeDeleteTiming); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(EntityState state, bool async) { await base.Load_one_to_one_PK_to_PK_reference_to_principal_already_loaded(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(EntityState state, bool async) { await base.Load_one_to_one_PK_to_PK_reference_to_dependent_already_loaded(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_collection_using_Query_already_loaded( @@ -783,7 +903,7 @@ public override async Task Load_collection_using_Query_already_loaded( await base.Load_collection_using_Query_already_loaded(state, async, cascadeDeleteTiming); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -797,7 +917,9 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_a await base.Load_many_to_one_reference_to_principal_using_Query_already_loaded(state, async); AssertSql( -""" + state == EntityState.Deleted + ? "" + : """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -811,7 +933,9 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_al await base.Load_one_to_one_reference_to_principal_using_Query_already_loaded(state, async); AssertSql( -""" + state == EntityState.Deleted + ? "" + : """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -828,7 +952,7 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_al await base.Load_one_to_one_reference_to_dependent_using_Query_already_loaded(state, async, cascadeDeleteTiming); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT TOP(2) [s].[Id], [s].[ParentId] @@ -842,7 +966,7 @@ public override async Task Load_one_to_one_PK_to_PK_reference_to_principal_using await base.Load_one_to_one_PK_to_PK_reference_to_principal_using_Query_already_loaded(state, async); AssertSql( -""" + """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -856,7 +980,7 @@ public override async Task Load_one_to_one_PK_to_PK_reference_to_dependent_using await base.Load_one_to_one_PK_to_PK_reference_to_dependent_using_Query_already_loaded(state, async); AssertSql( -""" + """ @__p_0='707' SELECT TOP(2) [s].[Id] @@ -870,7 +994,7 @@ public override async Task Load_collection_untyped(EntityState state, bool async await base.Load_collection_untyped(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -884,10 +1008,10 @@ public override async Task Load_many_to_one_reference_to_principal_untyped(Entit await base.Load_many_to_one_reference_to_principal_untyped(state, async); AssertSql( -""" + """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -898,10 +1022,10 @@ public override async Task Load_one_to_one_reference_to_principal_untyped(Entity await base.Load_one_to_one_reference_to_principal_untyped(state, async); AssertSql( -""" + """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -912,10 +1036,10 @@ public override async Task Load_one_to_one_reference_to_dependent_untyped(Entity await base.Load_one_to_one_reference_to_dependent_untyped(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -926,7 +1050,7 @@ public override async Task Load_collection_using_Query_untyped(EntityState state await base.Load_collection_using_Query_untyped(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -940,7 +1064,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_u await base.Load_many_to_one_reference_to_principal_using_Query_untyped(state, async); AssertSql( -""" + """ @__p_0='707' SELECT [p].[Id], [p].[AlternateId] @@ -954,7 +1078,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_un await base.Load_one_to_one_reference_to_principal_using_Query_untyped(state, async); AssertSql( -""" + """ @__p_0='707' SELECT [p].[Id], [p].[AlternateId] @@ -968,7 +1092,9 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_un await base.Load_one_to_one_reference_to_dependent_using_Query_untyped(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' (Nullable = true) SELECT [s].[Id], [s].[ParentId] @@ -982,7 +1108,7 @@ public override async Task Load_collection_not_found_untyped(EntityState state, await base.Load_collection_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -996,10 +1122,10 @@ public override async Task Load_many_to_one_reference_to_principal_not_found_unt await base.Load_many_to_one_reference_to_principal_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -1010,10 +1136,10 @@ public override async Task Load_one_to_one_reference_to_principal_not_found_unty await base.Load_one_to_one_reference_to_principal_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='787' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -1024,10 +1150,10 @@ public override async Task Load_one_to_one_reference_to_dependent_not_found_unty await base.Load_one_to_one_reference_to_dependent_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [Single] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -1038,7 +1164,7 @@ public override async Task Load_collection_using_Query_not_found_untyped(EntityS await base.Load_collection_using_Query_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -1052,7 +1178,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n await base.Load_many_to_one_reference_to_principal_using_Query_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='787' SELECT [p].[Id], [p].[AlternateId] @@ -1066,7 +1192,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_no await base.Load_one_to_one_reference_to_principal_using_Query_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='787' SELECT [p].[Id], [p].[AlternateId] @@ -1080,7 +1206,7 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_no await base.Load_one_to_one_reference_to_dependent_using_Query_not_found_untyped(state, async); AssertSql( -""" + """ @__p_0='767' (Nullable = true) SELECT [s].[Id], [s].[ParentId] @@ -1093,21 +1219,21 @@ public override async Task Load_collection_already_loaded_untyped(EntityState st { await base.Load_collection_already_loaded_untyped(state, async, cascadeDeleteTiming); - AssertSql(@""); + AssertSql(); } public override async Task Load_many_to_one_reference_to_principal_already_loaded_untyped(EntityState state, bool async) { await base.Load_many_to_one_reference_to_principal_already_loaded_untyped(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_principal_already_loaded_untyped(EntityState state, bool async) { await base.Load_one_to_one_reference_to_principal_already_loaded_untyped(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_dependent_already_loaded_untyped( @@ -1117,7 +1243,7 @@ public override async Task Load_one_to_one_reference_to_dependent_already_loaded { await base.Load_one_to_one_reference_to_dependent_already_loaded_untyped(state, async, cascadeDeleteTiming); - AssertSql(@""); + AssertSql(); } public override async Task Load_collection_using_Query_already_loaded_untyped( @@ -1128,7 +1254,7 @@ public override async Task Load_collection_using_Query_already_loaded_untyped( await base.Load_collection_using_Query_already_loaded_untyped(state, async, cascadeDeleteTiming); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -1142,7 +1268,9 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_a await base.Load_many_to_one_reference_to_principal_using_Query_already_loaded_untyped(state, async); AssertSql( -""" + state == EntityState.Deleted + ? "" + : """ @__p_0='707' SELECT [p].[Id], [p].[AlternateId] @@ -1156,7 +1284,9 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_al await base.Load_one_to_one_reference_to_principal_using_Query_already_loaded_untyped(state, async); AssertSql( -""" + state == EntityState.Deleted + ? "" + : """ @__p_0='707' SELECT [p].[Id], [p].[AlternateId] @@ -1173,7 +1303,7 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_al await base.Load_one_to_one_reference_to_dependent_using_Query_already_loaded_untyped(state, async, cascadeDeleteTiming); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [s].[Id], [s].[ParentId] @@ -1187,7 +1317,7 @@ public override async Task Load_collection_alternate_key(EntityState state, bool await base.Load_collection_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) SELECT [c].[Id], [c].[ParentId] @@ -1201,10 +1331,10 @@ public override async Task Load_many_to_one_reference_to_principal_alternate_key await base.Load_many_to_one_reference_to_principal_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 """); @@ -1215,10 +1345,10 @@ public override async Task Load_one_to_one_reference_to_principal_alternate_key( await base.Load_one_to_one_reference_to_principal_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 """); @@ -1229,10 +1359,10 @@ public override async Task Load_one_to_one_reference_to_dependent_alternate_key( await base.Load_one_to_one_reference_to_dependent_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [SingleAk] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -1243,7 +1373,7 @@ public override async Task Load_collection_using_Query_alternate_key(EntityState await base.Load_collection_using_Query_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) SELECT [c].[Id], [c].[ParentId] @@ -1257,7 +1387,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_a await base.Load_many_to_one_reference_to_principal_using_Query_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -1271,7 +1401,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_al await base.Load_one_to_one_reference_to_principal_using_Query_alternate_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -1285,7 +1415,9 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_al await base.Load_one_to_one_reference_to_dependent_using_Query_alternate_key(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='Root' (Size = 450) SELECT TOP(2) [s].[Id], [s].[ParentId] @@ -1298,14 +1430,14 @@ public override async Task Load_many_to_one_reference_to_principal_null_FK_alter { await base.Load_many_to_one_reference_to_principal_null_FK_alternate_key(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_principal_null_FK_alternate_key(EntityState state, bool async) { await base.Load_one_to_one_reference_to_principal_null_FK_alternate_key(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_alternate_key(EntityState state, bool async) @@ -1313,7 +1445,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n await base.Load_many_to_one_reference_to_principal_using_Query_null_FK_alternate_key(state, async); AssertSql( -""" + """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -1325,7 +1457,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu await base.Load_one_to_one_reference_to_principal_using_Query_null_FK_alternate_key(state, async); AssertSql( -""" + """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -1337,7 +1469,7 @@ public override async Task Load_collection_shadow_fk(EntityState state, bool asy await base.Load_collection_shadow_fk(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -1351,10 +1483,12 @@ public override async Task Load_many_to_one_reference_to_principal_shadow_fk(Ent await base.Load_many_to_one_reference_to_principal_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -1365,10 +1499,12 @@ public override async Task Load_one_to_one_reference_to_principal_shadow_fk(Enti await base.Load_one_to_one_reference_to_principal_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[Id] = @__p_0 """); @@ -1379,10 +1515,10 @@ public override async Task Load_one_to_one_reference_to_dependent_shadow_fk(Enti await base.Load_one_to_one_reference_to_dependent_shadow_fk(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentId] FROM [SingleShadowFk] AS [s] WHERE [s].[ParentId] = @__p_0 """); @@ -1393,7 +1529,7 @@ public override async Task Load_collection_using_Query_shadow_fk(EntityState sta await base.Load_collection_using_Query_shadow_fk(state, async); AssertSql( -""" + """ @__p_0='707' (Nullable = true) SELECT [c].[Id], [c].[ParentId] @@ -1407,7 +1543,9 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_s await base.Load_many_to_one_reference_to_principal_using_Query_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -1421,7 +1559,9 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_sh await base.Load_one_to_one_reference_to_principal_using_Query_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' SELECT TOP(2) [p].[Id], [p].[AlternateId] @@ -1435,7 +1575,9 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_sh await base.Load_one_to_one_reference_to_dependent_using_Query_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='707' (Nullable = true) SELECT TOP(2) [s].[Id], [s].[ParentId] @@ -1448,14 +1590,14 @@ public override async Task Load_many_to_one_reference_to_principal_null_FK_shado { await base.Load_many_to_one_reference_to_principal_null_FK_shadow_fk(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_principal_null_FK_shadow_fk(EntityState state, bool async) { await base.Load_one_to_one_reference_to_principal_null_FK_shadow_fk(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(EntityState state, bool async) @@ -1463,7 +1605,9 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n await base.Load_many_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -1475,7 +1619,9 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu await base.Load_one_to_one_reference_to_principal_using_Query_null_FK_shadow_fk(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -1487,7 +1633,7 @@ public override async Task Load_collection_composite_key(EntityState state, bool await base.Load_collection_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) @@ -1502,11 +1648,11 @@ public override async Task Load_many_to_one_reference_to_principal_composite_key await base.Load_many_to_one_reference_to_principal_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 AND [p].[Id] = @__p_1 """); @@ -1517,11 +1663,11 @@ public override async Task Load_one_to_one_reference_to_principal_composite_key( await base.Load_one_to_one_reference_to_principal_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' -SELECT [p].[Id], [p].[AlternateId] +SELECT TOP(1) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE [p].[AlternateId] = @__p_0 AND [p].[Id] = @__p_1 """); @@ -1532,11 +1678,11 @@ public override async Task Load_one_to_one_reference_to_dependent_composite_key( await base.Load_one_to_one_reference_to_dependent_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) -SELECT [s].[Id], [s].[ParentAlternateId], [s].[ParentId] +SELECT TOP(1) [s].[Id], [s].[ParentAlternateId], [s].[ParentId] FROM [SingleCompositeKey] AS [s] WHERE [s].[ParentAlternateId] = @__p_0 AND [s].[ParentId] = @__p_1 """); @@ -1547,7 +1693,7 @@ public override async Task Load_collection_using_Query_composite_key(EntityState await base.Load_collection_using_Query_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) @@ -1562,7 +1708,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_c await base.Load_many_to_one_reference_to_principal_using_Query_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' @@ -1577,7 +1723,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_co await base.Load_one_to_one_reference_to_principal_using_Query_composite_key(state, async); AssertSql( -""" + """ @__p_0='Root' (Size = 450) @__p_1='707' @@ -1592,7 +1738,9 @@ public override async Task Load_one_to_one_reference_to_dependent_using_Query_co await base.Load_one_to_one_reference_to_dependent_using_Query_composite_key(state, async); AssertSql( -""" + state == EntityState.Detached + ? "" + : """ @__p_0='Root' (Size = 450) @__p_1='707' (Nullable = true) @@ -1606,14 +1754,14 @@ public override async Task Load_many_to_one_reference_to_principal_null_FK_compo { await base.Load_many_to_one_reference_to_principal_null_FK_composite_key(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_one_to_one_reference_to_principal_null_FK_composite_key(EntityState state, bool async) { await base.Load_one_to_one_reference_to_principal_null_FK_composite_key(state, async); - AssertSql(@""); + AssertSql(); } public override async Task Load_many_to_one_reference_to_principal_using_Query_null_FK_composite_key(EntityState state, bool async) @@ -1621,7 +1769,7 @@ public override async Task Load_many_to_one_reference_to_principal_using_Query_n await base.Load_many_to_one_reference_to_principal_using_Query_null_FK_composite_key(state, async); AssertSql( -""" + """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -1633,7 +1781,7 @@ public override async Task Load_one_to_one_reference_to_principal_using_Query_nu await base.Load_one_to_one_reference_to_principal_using_Query_null_FK_composite_key(state, async); AssertSql( -""" + """ SELECT TOP(2) [p].[Id], [p].[AlternateId] FROM [Parent] AS [p] WHERE 0 = 1 @@ -1649,12 +1797,14 @@ protected override void RecordLog() private const string FileNewLine = @" "; - private void AssertSql(string expected) + private void AssertSql(string expected = null) { + var sql = Sql ?? ""; + expected ??= ""; try { Assert.Equal( - expected, Sql, ignoreLineEndingDifferences: true); + expected, sql, ignoreLineEndingDifferences: true); } catch { @@ -1679,7 +1829,7 @@ private void AssertSql(string expected) var testInfo = testName + " : " + lineNumber + FileNewLine; var newBaseLine = $@" AssertSql( - {"@\"" + Sql.Replace("\"", "\"\"") + "\""}); + {"@\"" + sql.Replace("\"", "\"\"") + "\""}); "; diff --git a/test/EFCore.Tests/Metadata/Conventions/ServicePropertyDiscoveryConventionTest.cs b/test/EFCore.Tests/Metadata/Conventions/ServicePropertyDiscoveryConventionTest.cs index 112f881b2d5..5df53826526 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ServicePropertyDiscoveryConventionTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ServicePropertyDiscoveryConventionTest.cs @@ -28,11 +28,8 @@ public void Finds_service_properties_in_hierarchy() foreach (var entityType in entityTypes) { ValidateServiceProperty(entityType, "Context"); - ValidateServiceProperty(entityType, "Context2"); ValidateServiceProperty(entityType, "EntityType"); - ValidateServiceProperty(entityType, "EntityType2"); ValidateServiceProperty(entityType, "ALazyLoader"); - ValidateServiceProperty(entityType, "ALazyLoader2"); ValidateServiceProperty, ILazyLoader>(entityType, "LazyLoader"); var clrType = entityType.ClrType; @@ -42,31 +39,22 @@ public void Finds_service_properties_in_hierarchy() } var contextProperty = clrType.GetAnyProperty("Context"); - var context2Property = clrType.GetAnyProperty("Context2"); var entityTypeProperty = clrType.GetAnyProperty("EntityType"); - var entityType2Property = clrType.GetAnyProperty("EntityType2"); var lazyLoaderProperty = clrType.GetAnyProperty("ALazyLoader"); - var lazyLoader2Property = clrType.GetAnyProperty("ALazyLoader2"); var lazyLoaderServiceProperty = clrType.GetAnyProperty("LazyLoader"); var entity = Activator.CreateInstance(entityType.ClrType); Assert.Null(contextProperty!.GetValue(entity)); - Assert.Null(context2Property!.GetValue(entity)); Assert.Null(entityTypeProperty!.GetValue(entity)); - Assert.Null(entityType2Property!.GetValue(entity)); Assert.Null(lazyLoaderProperty!.GetValue(entity)); - Assert.Null(lazyLoader2Property!.GetValue(entity)); Assert.Null(lazyLoaderServiceProperty!.GetValue(entity)); context.Add(entity!); Assert.Same(context, contextProperty!.GetValue(entity)); - Assert.Same(context, context2Property!.GetValue(entity)); Assert.Same(entityType, entityTypeProperty!.GetValue(entity)); - Assert.Same(entityType, entityType2Property!.GetValue(entity)); Assert.NotNull(lazyLoaderProperty!.GetValue(entity)); - Assert.NotNull(lazyLoader2Property!.GetValue(entity)); Assert.NotNull(lazyLoaderServiceProperty!.GetValue(entity)); } @@ -100,11 +88,8 @@ public void Finds_service_properties_in_hierarchy() } Assert.Same(context, clrType.GetAnyProperty("Context")!.GetValue(entry.Entity)); - Assert.Same(context, clrType.GetAnyProperty("Context2")!.GetValue(entry.Entity)); Assert.Same(entry.Metadata, clrType.GetAnyProperty("EntityType")!.GetValue(entry.Entity)); - Assert.Same(entry.Metadata, clrType.GetAnyProperty("EntityType2")!.GetValue(entry.Entity)); Assert.NotNull(clrType.GetAnyProperty("ALazyLoader")!.GetValue(entry.Entity)); - Assert.NotNull(clrType.GetAnyProperty("ALazyLoader2")!.GetValue(entry.Entity)); Assert.NotNull(clrType.GetAnyProperty("LazyLoader")!.GetValue(entry.Entity)); } } @@ -255,11 +240,8 @@ protected internal override void OnModelCreating(ModelBuilder modelBuilder) { // Because private properties on un-mapped base types are not found by convention b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("Context")!); - b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("Context2")!); b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("EntityType")!); - b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("EntityType2")!); b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("ALazyLoader")!); - b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("ALazyLoader2")!); b.Metadata.AddServiceProperty(typeof(PrivateUnmappedBase).GetAnyProperty("LazyLoader")!); }); } @@ -268,11 +250,8 @@ protected class PrivateUnmappedBase { public int Id { get; set; } private DbContext? Context { get; set; } - private DbContext? Context2 { get; set; } private IEntityType? EntityType { get; set; } - private IEntityType? EntityType2 { get; set; } private ILazyLoader? ALazyLoader { get; set; } - private ILazyLoader? ALazyLoader2 { get; set; } private Action? LazyLoader { get; set; } } @@ -288,11 +267,8 @@ protected class PrivateMappedBase { public int Id { get; set; } private DbContext? Context { get; set; } - private DbContext? Context2 { get; set; } private IEntityType? EntityType { get; set; } - private IEntityType? EntityType2 { get; set; } private ILazyLoader? ALazyLoader { get; set; } - private ILazyLoader? ALazyLoader2 { get; set; } private Action? LazyLoader { get; set; } } @@ -313,28 +289,19 @@ public PublicUnmappedBase() public PublicUnmappedBase( int id, DbContext? context, - DbContext? context2, IEntityType? entityType, - IEntityType? entityType2, ILazyLoader? aLazyLoader, - ILazyLoader? aLazyLoader2, Action? lazyLoader) { Id = id; Context = context; - Context2 = context2; EntityType = entityType; - EntityType2 = entityType2; ALazyLoader = aLazyLoader; - ALazyLoader2 = aLazyLoader2; LazyLoader = lazyLoader; Assert.NotNull(context); - Assert.NotNull(context2); Assert.NotNull(entityType); - Assert.NotNull(entityType2); Assert.NotNull(aLazyLoader); - Assert.NotNull(aLazyLoader2); Assert.NotNull(lazyLoader); ConstructorCalled = true; @@ -342,11 +309,8 @@ public PublicUnmappedBase( public int Id { get; set; } public DbContext? Context { get; set; } - public DbContext? Context2 { get; set; } public IEntityType? EntityType { get; set; } - public IEntityType? EntityType2 { get; set; } public ILazyLoader? ALazyLoader { get; set; } - public ILazyLoader? ALazyLoader2 { get; set; } public Action? LazyLoader { get; set; } [NotMapped] @@ -362,13 +326,10 @@ public PublicUnmappedBaseSuper() public PublicUnmappedBaseSuper( int id, DbContext? context, - DbContext? context2, IEntityType? entityType, - IEntityType? entityType2, ILazyLoader? aLazyLoader, - ILazyLoader? aLazyLoader2, Action? lazyLoader) - : base(id, context, context2, entityType, entityType2, aLazyLoader, aLazyLoader2, lazyLoader) + : base(id, context, entityType, aLazyLoader, lazyLoader) { } } @@ -382,13 +343,10 @@ public PublicUnmappedBaseSub() public PublicUnmappedBaseSub( int id, DbContext? context, - DbContext? context2, IEntityType? entityType, - IEntityType? entityType2, ILazyLoader? aLazyLoader, - ILazyLoader? aLazyLoader2, Action? lazyLoader) - : base(id, context, context2, entityType, entityType2, aLazyLoader, aLazyLoader2, lazyLoader) + : base(id, context, entityType, aLazyLoader, lazyLoader) { } } @@ -397,11 +355,8 @@ protected class PublicMappedBase { public int Id { get; set; } public DbContext? Context { get; set; } - public DbContext? Context2 { get; set; } public IEntityType? EntityType { get; set; } - public IEntityType? EntityType2 { get; set; } public ILazyLoader? ALazyLoader { get; set; } - public ILazyLoader? ALazyLoader2 { get; set; } public Action? LazyLoader { get; set; } } @@ -417,33 +372,24 @@ protected class PrivateWithDuplicatesBase { public int Id { get; set; } private DbContext? Context { get; set; } - private DbContext? Context2 { get; set; } private IEntityType? EntityType { get; set; } - private IEntityType? EntityType2 { get; set; } private ILazyLoader? ALazyLoader { get; set; } - private ILazyLoader? ALazyLoader2 { get; set; } private Action? LazyLoader { get; set; } } protected class PrivateWithDuplicatesSuper : PrivateWithDuplicatesBase { private DbContext? Context { get; set; } - private DbContext? Context2 { get; set; } private IEntityType? EntityType { get; set; } - private IEntityType? EntityType2 { get; set; } private ILazyLoader? ALazyLoader { get; set; } - private ILazyLoader? ALazyLoader2 { get; set; } private Action? LazyLoader { get; set; } } protected class PrivateWithDuplicatesSub : PrivateWithDuplicatesSuper { private DbContext? Context { get; set; } - private DbContext? Context2 { get; set; } private IEntityType? EntityType { get; set; } - private IEntityType? EntityType2 { get; set; } private ILazyLoader? ALazyLoader { get; set; } - private ILazyLoader? ALazyLoader2 { get; set; } private Action? LazyLoader { get; set; } } } From 3c1fdd72273b1ce50e9323a43a18a41dbcf2db8e Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 17 Dec 2022 00:06:25 +0000 Subject: [PATCH 2/3] Use reference equality in recursive loading check --- .../Infrastructure/Internal/LazyLoader.cs | 44 ++++++++++++++++--- .../LazyLoadProxyTestBase.cs | 6 +++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/EFCore/Infrastructure/Internal/LazyLoader.cs b/src/EFCore/Infrastructure/Internal/LazyLoader.cs index b75c3bb19f4..6a6eaf02071 100644 --- a/src/EFCore/Infrastructure/Internal/LazyLoader.cs +++ b/src/EFCore/Infrastructure/Internal/LazyLoader.cs @@ -90,11 +90,11 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName Check.NotEmpty(navigationName, nameof(navigationName)); var navEntry = (entity, navigationName); - if (!(_isLoading ??= new List<(object Entity, string NavigationName)>()).Contains(navEntry)) + if (!IsLoading(navEntry)) { try { - _isLoading.Add(navEntry); + _isLoading!.Add(navEntry); if (ShouldLoad(entity, navigationName, out var entry)) { try @@ -110,7 +110,7 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName } finally { - _isLoading.Remove(navEntry); + DoneLoading(navEntry); } } } @@ -130,11 +130,11 @@ public virtual async Task LoadAsync( Check.NotEmpty(navigationName, nameof(navigationName)); var navEntry = (entity, navigationName); - if (!(_isLoading ??= new List<(object Entity, string NavigationName)>()).Contains(navEntry)) + if (!IsLoading(navEntry)) { try { - _isLoading.Add(navEntry); + _isLoading!.Add(navEntry); if (ShouldLoad(entity, navigationName, out var entry)) { try @@ -150,11 +150,43 @@ public virtual async Task LoadAsync( } finally { - _isLoading.Remove(navEntry); + DoneLoading(navEntry); } } } + private bool IsLoading((object Entity, string NavigationName) navEntry) + => (_isLoading ??= new List<(object Entity, string NavigationName)>()) + .Contains(navEntry, EntityNavigationEqualityComparer.Instance); + + private void DoneLoading((object Entity, string NavigationName) navEntry) + { + for (var i = 0; i < _isLoading!.Count; i++) + { + if (EntityNavigationEqualityComparer.Instance.Equals(navEntry, _isLoading[i])) + { + _isLoading.RemoveAt(i); + break; + } + } + } + + private sealed class EntityNavigationEqualityComparer : IEqualityComparer<(object Entity, string NavigationName)> + { + public static readonly EntityNavigationEqualityComparer Instance = new(); + + private EntityNavigationEqualityComparer() + { + } + + public bool Equals((object Entity, string NavigationName) x, (object Entity, string NavigationName) y) + => ReferenceEquals(x.Entity, y.Entity) + && string.Equals(x.NavigationName, y.NavigationName, StringComparison.Ordinal); + + public int GetHashCode((object Entity, string NavigationName) obj) + => HashCode.Combine(obj.Entity.GetHashCode(), obj.GetHashCode()); + } + private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)] out NavigationEntry? navigationEntry) { if (!_detached && !IsLoaded(entity, navigationName)) diff --git a/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs b/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs index 76e81817fc5..e87ff6e3c8f 100644 --- a/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs +++ b/test/EFCore.Specification.Tests/LazyLoadProxyTestBase.cs @@ -3064,6 +3064,12 @@ public int IdLoadedFromParent } set => _backing = value; } + + public override bool Equals(object obj) + => throw new InvalidOperationException(); + + public override int GetHashCode() + => throw new InvalidOperationException(); } public class Child From d9505e63e5df3ced288bf4a4381f0c25b25ff023 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 17 Dec 2022 11:52:58 +0000 Subject: [PATCH 3/3] Review updates. --- src/EFCore/Infrastructure/Internal/LazyLoader.cs | 2 ++ test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Infrastructure/Internal/LazyLoader.cs b/src/EFCore/Infrastructure/Internal/LazyLoader.cs index 6a6eaf02071..b0f4018b891 100644 --- a/src/EFCore/Infrastructure/Internal/LazyLoader.cs +++ b/src/EFCore/Infrastructure/Internal/LazyLoader.cs @@ -95,6 +95,7 @@ public virtual void Load(object entity, [CallerMemberName] string navigationName try { _isLoading!.Add(navEntry); + // ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138. if (ShouldLoad(entity, navigationName, out var entry)) { try @@ -135,6 +136,7 @@ public virtual async Task LoadAsync( try { _isLoading!.Add(navEntry); + // ShouldLoad is called after _isLoading.Add because it could attempt to load the property. See #13138. if (ShouldLoad(entity, navigationName, out var entry)) { try diff --git a/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs index 9b67bc6cd95..4aeb755b51a 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/WarningsTest.cs @@ -208,7 +208,7 @@ public void Lazy_loading_is_logged_only_when_actually_loading() loggerFactory.Log.Select(l => l.Message)); var entityEntry = context.Entry(entity); - var loaded = entityEntry.Navigation("Nav").IsLoaded; + Assert.True(entityEntry.Navigation("Nav").IsLoaded); loggerFactory.Clear(); Assert.NotNull(entity.Nav); Assert.DoesNotContain(