diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index c47d9ab804c..f3ab223c6ae 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -639,6 +639,15 @@ protected virtual void MarkShadowPropertiesNotSet([NotNull] IEntityType entityTy } } + /// + /// 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 void MarkUnknown([NotNull] IProperty property) + => _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Unknown, true); + internal static readonly MethodInfo ReadShadowValueMethod = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadShadowValue)); @@ -1286,6 +1295,13 @@ public virtual InternalEntityEntry PrepareToSave() property.Name, EntityType.DisplayName())); } + + if (property.IsKey() + && property.IsForeignKey() + && _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown)) + { + throw new InvalidOperationException(CoreStrings.UnknownKeyValue(entityType.DisplayName(), property.Name)); + } } } else if (EntityState == EntityState.Modified) diff --git a/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs b/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs index 90c96e200dc..867b36606bc 100644 --- a/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs +++ b/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs @@ -71,6 +71,8 @@ public virtual InternalEntityEntry PropagateValue(InternalEntityEntry entry, IPr { entry[property] = value; } + + entry.MarkUnknown(property); } } @@ -108,6 +110,8 @@ public virtual async Task PropagateValueAsync( { entry[property] = value; } + + entry.MarkUnknown(property); } } diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 85f83f0a9b1..a11d53abd23 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -514,6 +514,14 @@ public static string TempValuePersists([CanBeNull] object property, [CanBeNull] GetString("TempValuePersists", nameof(property), nameof(entityType), nameof(state)), property, entityType, state); + /// + /// The value of '{entityType}.{property}' is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known. + /// + public static string UnknownKeyValue([CanBeNull] object entityType, [CanBeNull] object property) + => string.Format( + GetString("UnknownKeyValue", nameof(entityType), nameof(property)), + entityType, property); + /// /// The EF.Property<T> method may only be used within LINQ queries. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index d73698fd7fb..7d82bbd8f5e 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -322,6 +322,9 @@ The property '{property}' on entity type '{entityType}' has a temporary value while attempting to change the entity's state to '{state}'. Either set a permanent value explicitly or ensure that the database is configured to generate values for this property. + + The value of '{entityType}.{property}' is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known. + An exception occurred while iterating over the results of a query for context type '{contextType}'.{newline}{error} Error CoreEventId.QueryIterationFailed Type string Exception diff --git a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs index 39f52e7dd88..2196b49a9c6 100644 --- a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs @@ -26,6 +26,57 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking { public class ChangeTrackerTest { + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public async Task Keys_generated_on_behalf_of_a_principal_are_not_saved(bool async) + { + using var context = new WeakHerosContext(); + + var entity = new Weak { Id = Guid.NewGuid() }; + + if (async) + { + await context.AddAsync(entity); + } + else + { + context.Add(entity); + } + + Assert.Equal( + CoreStrings.UnknownKeyValue(nameof(Weak), nameof(Weak.HeroId)), + Assert.Throws(() => context.SaveChanges()).Message); + } + + public class Hero + { + public Guid Id { get; set; } + public ICollection Weaks { get; set; } + } + + public class Weak + { + public Guid Id { get; set; } + public Guid HeroId { get; set; } + + public Hero Hero { get; set; } + } + + public class WeakHerosContext : DbContext + { + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity( + b => + { + b.HasKey(e => new { e.Id, e.HeroId }); + b.HasOne(e => e.Hero).WithMany(e => e.Weaks).HasForeignKey(e => e.HeroId); + }); + + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseInMemoryDatabase(nameof(WeakHerosContext)); + } + [ConditionalFact] public void DetectChanges_is_logged() {