diff --git a/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs index a428282b5a2..42ffe9c8842 100644 --- a/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs @@ -13,6 +13,9 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal; /// public class ShadowValuesFactoryFactory : SnapshotFactoryFactory { + private static readonly bool UseOldBehavior30764 + = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30764", out var enabled) && enabled; + /// /// 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 @@ -20,8 +23,7 @@ public class ShadowValuesFactoryFactory : SnapshotFactoryFactory /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override int GetPropertyIndex(IPropertyBase propertyBase) - // Navigations are not included in the supplied value buffer - => (propertyBase as IProperty)?.GetShadowIndex() ?? -1; + => UseOldBehavior30764 ? ((propertyBase as IProperty)?.GetShadowIndex() ?? -1) : propertyBase.GetShadowIndex(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index 112729c8681..15016ee4e8a 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -43,7 +43,7 @@ protected virtual Expression CreateConstructorExpression( var count = GetPropertyCount(entityType); var types = new Type[count]; - var propertyBases = new IPropertyBase[count]; + var propertyBases = new IPropertyBase?[count]; foreach (var propertyBase in entityType.GetPropertiesAndNavigations()) { @@ -95,7 +95,7 @@ protected virtual Expression CreateSnapshotExpression( Type? entityType, ParameterExpression parameter, Type[] types, - IList propertyBases) + IList propertyBases) { var count = types.Length; diff --git a/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs index cc9e963e82f..65d6534b23d 100644 --- a/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs @@ -23,7 +23,7 @@ protected override Expression CreateSnapshotExpression( [DynamicallyAccessedMembers(IEntityType.DynamicallyAccessedMemberTypes)] Type? entityType, ParameterExpression parameter, Type[] types, - IList propertyBases) + IList propertyBases) { var constructorExpression = Expression.Convert( Expression.New( diff --git a/src/EFCore/Infrastructure/ExpressionExtensions.cs b/src/EFCore/Infrastructure/ExpressionExtensions.cs index 5b7cf16c4c5..6be2b236a36 100644 --- a/src/EFCore/Infrastructure/ExpressionExtensions.cs +++ b/src/EFCore/Infrastructure/ExpressionExtensions.cs @@ -22,6 +22,9 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure; /// public static class ExpressionExtensions { + private static readonly bool UseOldBehavior30764 + = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30764", out var enabled) && enabled; + /// /// Creates a printable string representation of the given expression. /// @@ -288,11 +291,13 @@ public static Expression CreateValueBufferReadValueExpression( Type type, int index, IPropertyBase? property) - => Expression.Call( - MakeValueBufferTryReadValueMethod(type), - valueBuffer, - Expression.Constant(index), - Expression.Constant(property, typeof(IPropertyBase))); + => (property is INavigationBase && !UseOldBehavior30764) + ? Expression.Constant(null, typeof(object)) + : Expression.Call( + MakeValueBufferTryReadValueMethod(type), + valueBuffer, + Expression.Constant(index), + Expression.Constant(property, typeof(IPropertyBase))); /// /// diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs index bab4b936cd4..5880f6d434f 100644 --- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs @@ -32,6 +32,9 @@ namespace Microsoft.EntityFrameworkCore.Query; /// public abstract class ShapedQueryCompilingExpressionVisitor : ExpressionVisitor { + private static readonly bool UseOldBehavior30764 + = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30764", out var enabled) && enabled; + private static readonly PropertyInfo CancellationTokenMemberInfo = typeof(QueryContext).GetTypeInfo().GetProperty(nameof(QueryContext.CancellationToken))!; @@ -586,7 +589,15 @@ private BlockExpression CreateFullMaterializeExpression( { var valueBufferExpression = Expression.Call( materializationContextVariable, MaterializationContext.GetValueBufferMethod); - var shadowProperties = concreteEntityType.GetProperties().Where(p => p.IsShadowProperty()); + + var shadowProperties = UseOldBehavior30764 + ? (IEnumerable)concreteEntityType.GetProperties() + .Where(p => p.IsShadowProperty()) + : concreteEntityType.GetProperties() + .Concat(concreteEntityType.GetNavigations()) + .Concat(concreteEntityType.GetSkipNavigations()) + .Where(n => n.IsShadowProperty()) + .OrderBy(e => e.GetShadowIndex()); blockExpressions.Add( Expression.Assign( diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs index 947855ffa43..f31bacd7f64 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBase.cs @@ -549,6 +549,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity(); modelBuilder.Entity(); modelBuilder.Entity(); + + modelBuilder.Entity().HasData( + new { Id = 1, Key = "root-1", Name = "Root One" }); + + modelBuilder.Entity().HasData( + new { Id = 4, Key = "root-1/leaf-1", Name = "Leaf One-One", RootId = 1 }); + + modelBuilder.Entity() + .HasMany(entity => entity.Entities) + .WithMany() + .UsingEntity(); } protected virtual object CreateFullGraph() @@ -3984,6 +3995,68 @@ public virtual SecondLaw SecondLaw } } + protected abstract class Parsnip2 : NotifyingEntity + { + private int _id; + + public int Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + } + + protected class Lettuce2 : Parsnip2 + { + private Beetroot2 _root; + + public Beetroot2 Root + { + get => _root; + set => SetWithNotify(value, ref _root); + } + } + + protected class RootStructure : NotifyingEntity + { + private Guid _radish2Id; + private int _parsnip2Id; + + public Guid Radish2Id + { + get => _radish2Id; + set => SetWithNotify(value, ref _radish2Id); + } + + public int Parsnip2Id + { + get => _parsnip2Id; + set => SetWithNotify(value, ref _parsnip2Id); + } + } + + protected class Radish2 : NotifyingEntity + { + private Guid _id; + private ICollection _entities = new ObservableHashSet(); + + public Guid Id + { + get => _id; + set => SetWithNotify(value, ref _id); + } + + public ICollection Entities + { + get => _entities; + set => SetWithNotify(value, ref _entities); + } + } + + protected class Beetroot2 : Parsnip2 + { + } + protected class NotifyingEntity : INotifyPropertyChanging, INotifyPropertyChanged { protected void SetWithNotify(T value, ref T field, [CallerMemberName] string propertyName = "") diff --git a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs index 2959e19cbcb..68d60def63e 100644 --- a/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs +++ b/test/EFCore.Specification.Tests/GraphUpdates/GraphUpdatesTestBaseMiscellaneous.cs @@ -1932,4 +1932,19 @@ private static SecondLaw AddSecondLevel(bool thirdLevel1, bool thirdLevel2) return secondLevel; } + + [ConditionalTheory] // Issue #30764 + [InlineData(false)] + [InlineData(true)] + public virtual Task Shadow_skip_navigation_in_base_class_is_handled(bool async) + => ExecuteWithStrategyInTransactionAsync( + async context => + { + var entities = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); + + Assert.Equal(1, entities.Count); + Assert.Equal(nameof(Lettuce2), context.Entry(entities[0]).Property("Discriminator").CurrentValue); + }); } diff --git a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs index f357c6c4902..532279eb4db 100644 --- a/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/GraphUpdates/GraphUpdatesSqlServerOwnedTest.cs @@ -38,6 +38,10 @@ public override Task Update_root_by_collection_replacement_of_deleted_third_leve public override Task Sever_relationship_that_will_later_be_deleted(bool async) => Task.CompletedTask; + // No owned types + public override Task Shadow_skip_navigation_in_base_class_is_handled(bool async) + => Task.CompletedTask; + // Owned dependents are always loaded public override void Required_one_to_one_are_cascade_deleted_in_store( CascadeTiming? cascadeDeleteTiming,