From fa6f5a6262ad56b0f254c1795063342c8354a79f Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Mon, 6 Sep 2021 19:12:29 +0100 Subject: [PATCH] Store store-generated FK values in the store-generated snapshot Fixes #22603 --- .../Internal/InternalEntityEntry.cs | 6 ++- .../StoreGeneratedValuesFactoryFactory.cs | 44 +++++++++++++++++++ src/EFCore/Metadata/Internal/EntityType.cs | 6 +-- .../Metadata/Internal/IRuntimeEntityType.cs | 2 +- src/EFCore/Metadata/RuntimeEntityType.cs | 6 +-- .../StoreGeneratedTestBase.cs | 21 ++++++++- .../StoreGeneratedSqlServerTest.cs | 35 ++++++++++++++- 7 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 src/EFCore/ChangeTracking/Internal/StoreGeneratedValuesFactoryFactory.cs diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 0356f722dce..b505052b521 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -613,6 +613,10 @@ public void PropagateValue( SetTemporaryValue(dependentProperty, principalValue); } } + else if (principalEntry.GetValueType(principalProperty) == CurrentValueType.StoreGenerated) + { + SetStoreGeneratedValue(dependentProperty, principalValue); + } else { SetProperty(dependentProperty, principalValue, isMaterialization, setModified); @@ -1052,7 +1056,7 @@ public void EnsureStoreGeneratedValues() { if (_storeGeneratedValues.IsEmpty) { - _storeGeneratedValues = new SidecarValues(((IRuntimeEntityType)EntityType).StoreGeneratedValuesFactory(this)); + _storeGeneratedValues = new SidecarValues(((IRuntimeEntityType)EntityType).StoreGeneratedValuesFactory()); } } diff --git a/src/EFCore/ChangeTracking/Internal/StoreGeneratedValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/StoreGeneratedValuesFactoryFactory.cs new file mode 100644 index 00000000000..0a51dd34f66 --- /dev/null +++ b/src/EFCore/ChangeTracking/Internal/StoreGeneratedValuesFactoryFactory.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking.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. + /// + public class StoreGeneratedValuesFactoryFactory : SidecarValuesFactoryFactory + { + /// + /// 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. + /// + protected override bool UseEntityVariable + => 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. + /// + protected override Expression CreateReadShadowValueExpression(ParameterExpression parameter, IPropertyBase property) + => Expression.Default(property.ClrType); + + /// + /// 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. + /// + protected override Expression CreateReadValueExpression(ParameterExpression parameter, IPropertyBase property) + => Expression.Default(property.ClrType); + } +} diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index 86920ccdd56..c07a89021a8 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -77,7 +77,7 @@ private readonly SortedDictionary _serviceProperties private Func? _relationshipSnapshotFactory; private Func? _originalValuesFactory; private Func? _temporaryValuesFactory; - private Func? _storeGeneratedValuesFactory; + private Func? _storeGeneratedValuesFactory; private Func? _shadowValuesFactory; private Func? _emptyShadowValuesFactory; private Func? _instanceFactory; @@ -2665,13 +2665,13 @@ public virtual Func OriginalValuesFactory /// 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 Func StoreGeneratedValuesFactory + public virtual Func StoreGeneratedValuesFactory => NonCapturingLazyInitializer.EnsureInitialized( ref _storeGeneratedValuesFactory, this, static entityType => { entityType.EnsureReadOnly(); - return new SidecarValuesFactoryFactory().Create(entityType); + return new StoreGeneratedValuesFactoryFactory().CreateEmpty(entityType); }); /// diff --git a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs index 86ce6e8887e..cd59f1313f7 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs @@ -45,7 +45,7 @@ public interface IRuntimeEntityType : IEntityType /// 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. /// - Func StoreGeneratedValuesFactory { get; } + Func StoreGeneratedValuesFactory { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index f260b51799b..638df57a49b 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -66,7 +66,7 @@ private readonly SortedDictionary _serviceProper private Func? _relationshipSnapshotFactory; private Func? _originalValuesFactory; private Func? _temporaryValuesFactory; - private Func? _storeGeneratedValuesFactory; + private Func? _storeGeneratedValuesFactory; private Func? _shadowValuesFactory; private Func? _emptyShadowValuesFactory; private Func? _instanceFactory; @@ -1258,10 +1258,10 @@ Func IRuntimeEntityType.OriginalValuesFactory static entityType => new OriginalValuesFactoryFactory().Create(entityType)); /// - Func IRuntimeEntityType.StoreGeneratedValuesFactory + Func IRuntimeEntityType.StoreGeneratedValuesFactory => NonCapturingLazyInitializer.EnsureInitialized( ref _storeGeneratedValuesFactory, this, - static entityType => new SidecarValuesFactoryFactory().Create(entityType)); + static entityType => new StoreGeneratedValuesFactoryFactory().CreateEmpty(entityType)); /// Func IRuntimeEntityType.TemporaryValuesFactory diff --git a/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs b/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs index 0ec16f0eb56..d40ff4536d8 100644 --- a/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs +++ b/test/EFCore.Specification.Tests/StoreGeneratedTestBase.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Reflection; @@ -1467,6 +1468,19 @@ public int Id } public string Name { get; set; } + + public ICollection MixedMetaphors { get; set; } + public Species Species { get; set; } + } + + protected class Species + { + public int Id { get; set; } + public string Name { get; set; } + + public int? DarwinId { get; set; } + public int? MetaphoricId { get; set; } + } protected class Gumball @@ -1915,7 +1929,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con property.SetAfterSaveBehavior(PropertySaveBehavior.Throw); }); - modelBuilder.Entity(); + modelBuilder.Entity( + b => + { + b.HasOne(e => e.Species).WithOne().HasForeignKey(e => e.DarwinId); + b.HasMany(e => e.MixedMetaphors).WithOne().HasForeignKey(e => e.MetaphoricId); + }); modelBuilder.Entity( b => diff --git a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs index 3c6e2997eb0..6726e148e09 100644 --- a/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/StoreGeneratedSqlServerTest.cs @@ -29,11 +29,32 @@ public virtual void Exception_in_SaveChanges_causes_store_values_to_be_reverted( var entities = new List(); for (var i = 0; i < 100; i++) { - entities.Add(new Darwin()); + entities.Add(new() + { + Species = new() { Name = "Goldfish (with legs)" }, + MixedMetaphors = new List + { + new() { Name = "Large ground finch" }, + new() { Name = "Medium ground finch" }, + new() { Name = "Small tree finch" }, + new() { Name = "Green warbler-finch" } + } + }); } entities.Add( - new Darwin { Id = 1777 }); + new() + { + Id = 1777, + Species = new() { Name = "Goldfish (with legs)" }, + MixedMetaphors = new List + { + new() { Name = "Large ground finch" }, + new() { Name = "Medium ground finch" }, + new() { Name = "Small tree finch" }, + new() { Name = "Green warbler-finch" } + } + }); for (var i = 0; i < 2; i++) { @@ -77,9 +98,19 @@ public virtual void Exception_in_SaveChanges_causes_store_values_to_be_reverted( { Assert.Equal(0, entity.Id); Assert.Null(entity._id); + Assert.Null(entity.Species.DarwinId); + foreach (var species in entity.MixedMetaphors) + { + Assert.Null(species.MetaphoricId); + } } Assert.Equal(1777, entities[100].Id); + Assert.Equal(1777, entities[100].Species.DarwinId); + foreach (var species in entities[100].MixedMetaphors) + { + Assert.Equal(1777, species.MetaphoricId); + } foreach (var entity in entities) {