From fc62a2146170a95f930e52365dd999f561dc53b8 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Thu, 8 Apr 2021 14:44:17 -0700 Subject: [PATCH] Allow property values to be explicitly flagged as temporary This allows values explicitly set by the application as temporary to be stored in and obtained from the entity instance. Fixes #23191 Fixes #24245 --- .../Internal/InternalEntityEntry.cs | 61 ++- .../ChangeTracking/Internal/KeyPropagator.cs | 9 +- .../Internal/NavigationFixer.cs | 9 +- .../ChangeTracking/Internal/StateData.cs | 11 +- src/EFCore/ChangeTracking/PropertyEntry.cs | 10 +- .../ChangeTracking/Internal/StateDataTest.cs | 38 +- .../ChangeTracking/TemporaryValuesTest.cs | 349 ++++++++++++++++++ 7 files changed, 449 insertions(+), 38 deletions(-) create mode 100644 test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 9c021e81ba4..8631fb4174b 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -552,19 +552,52 @@ public virtual bool IsConceptualNull(IProperty property) public virtual bool HasTemporaryValue(IProperty property) => GetValueType(property) == CurrentValueType.Temporary; + /// + /// 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 PropagateValue( + InternalEntityEntry principalEntry, + IProperty principalProperty, + IProperty dependentProperty, + bool isMaterialization = false, + bool setModified = true) + { + var principalValue = principalEntry[principalProperty]; + if (principalEntry.HasTemporaryValue(principalProperty)) + { + if (principalEntry._stateData.IsPropertyFlagged(principalProperty.GetIndex(), PropertyFlag.IsTemporary)) + { + SetProperty(dependentProperty, principalValue, isMaterialization, setModified); + _stateData.FlagProperty(dependentProperty.GetIndex(), PropertyFlag.IsTemporary, true); + } + else + { + SetTemporaryValue(dependentProperty, principalValue); + } + } + else + { + SetProperty(dependentProperty, principalValue, isMaterialization, setModified); + _stateData.FlagProperty(dependentProperty.GetIndex(), PropertyFlag.IsTemporary, false); + } + } + private CurrentValueType GetValueType( IProperty property, Func? equals = null) { - var tempIndex = property.GetStoreGeneratedIndex(); - if (tempIndex == -1) + if (_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary)) { - return CurrentValueType.Normal; + return CurrentValueType.Temporary; } - if (equals == null) + var tempIndex = property.GetStoreGeneratedIndex(); + if (tempIndex == -1) { - equals = ValuesEqualFunc(property); + return CurrentValueType.Normal; } if (!PropertyHasDefaultValue(property)) @@ -572,6 +605,7 @@ private CurrentValueType GetValueType( return CurrentValueType.Normal; } + @equals ??= ValuesEqualFunc(property); var defaultValue = property.ClrType.GetDefaultValue(); var value = ReadPropertyValue(property); if (!equals(value, defaultValue)) @@ -611,6 +645,15 @@ public virtual void SetTemporaryValue(IProperty property, object? value, bool se SetProperty(property, value, isMaterialization: false, setModified, isCascadeDelete: false, CurrentValueType.Temporary); } + /// + /// 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 MarkAsTemporary(IProperty property, bool temporary) + => _stateData.FlagProperty(property.GetIndex(), PropertyFlag.IsTemporary, temporary); + /// /// 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 @@ -1208,6 +1251,12 @@ private void SetProperty( if (valueType == CurrentValueType.StoreGenerated) { + var defaultValue = asProperty!.ClrType.GetDefaultValue(); + if (!equals(currentValue, defaultValue)) + { + WritePropertyValue(asProperty, defaultValue, isMaterialization); + } + EnsureStoreGeneratedValues(); _storeGeneratedValues.SetValue(asProperty!, value, storeGeneratedIndex); } @@ -1285,6 +1334,8 @@ public virtual void AcceptChanges() _storeGeneratedValues = new SidecarValues(); _temporaryValues = new SidecarValues(); } + + _stateData.FlagAllProperties(EntityType.PropertyCount(), PropertyFlag.IsTemporary, false); var currentState = EntityState; if ((currentState == EntityState.Unchanged) diff --git a/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs b/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs index 3c31be4b159..58cf77dc347 100644 --- a/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs +++ b/src/EFCore/ChangeTracking/Internal/KeyPropagator.cs @@ -162,14 +162,7 @@ public KeyPropagator( if (generationProperty == null || !principalProperty.ClrType.IsDefaultValue(principalValue)) { - if (principalEntry.HasTemporaryValue(principalProperty)) - { - entry.SetTemporaryValue(property, principalValue); - } - else - { - entry[property] = principalValue; - } + entry.PropagateValue(principalEntry, principalProperty, property); return principalEntry; } diff --git a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs index f3dfba37afd..ab7073d3487 100644 --- a/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs +++ b/src/EFCore/ChangeTracking/Internal/NavigationFixer.cs @@ -1213,14 +1213,7 @@ private static void SetForeignKeyProperties( || (dependentEntry.IsConceptualNull(dependentProperty) && principalValue != null)) { - if (principalEntry.HasTemporaryValue(principalProperty)) - { - dependentEntry.SetTemporaryValue(dependentProperty, principalValue, setModified); - } - else - { - dependentEntry.SetProperty(dependentProperty, principalValue, fromQuery, setModified); - } + dependentEntry.PropagateValue(principalEntry, principalProperty, dependentProperty, fromQuery, setModified); dependentEntry.StateManager.UpdateDependentMap(dependentEntry, foreignKey); dependentEntry.SetRelationshipSnapshotValue(dependentProperty, principalValue); diff --git a/src/EFCore/ChangeTracking/Internal/StateData.cs b/src/EFCore/ChangeTracking/Internal/StateData.cs index e10cedbcae0..214bb5c045f 100644 --- a/src/EFCore/ChangeTracking/Internal/StateData.cs +++ b/src/EFCore/ChangeTracking/Internal/StateData.cs @@ -12,20 +12,21 @@ internal enum PropertyFlag Modified = 0, Null = 1, Unknown = 2, - IsLoaded = 3 + IsLoaded = 3, + IsTemporary = 4 } internal readonly struct StateData { private const int BitsPerInt = 32; private const int BitsForEntityState = 3; - private const int BitsForEntityFlags = 1; - private const int BitsForPropertyFlags = 4; + private const int BitsForEntityFlags = 5; + private const int BitsForPropertyFlags = 8; private const int BitsForAdditionalState = BitsForEntityState + BitsForEntityFlags; private const int EntityStateMask = 0x07; - private const int UnusedStateMask = 0x08; // So entity state uses even number of bits + private const int UnusedStateMask = 0xF8; // So entity state uses even number of bits private const int AdditionalStateMask = EntityStateMask | UnusedStateMask; - private const int PropertyFlagMask = 0x11111111; + private const int PropertyFlagMask = 0x01010101; private readonly int[] _bits; diff --git a/src/EFCore/ChangeTracking/PropertyEntry.cs b/src/EFCore/ChangeTracking/PropertyEntry.cs index 8fefa7abec1..b61abb42bbf 100644 --- a/src/EFCore/ChangeTracking/PropertyEntry.cs +++ b/src/EFCore/ChangeTracking/PropertyEntry.cs @@ -63,14 +63,8 @@ public virtual bool IsTemporary get => InternalEntry.HasTemporaryValue(Metadata); set { - if (value) - { - InternalEntry.SetTemporaryValue(Metadata, CurrentValue); - } - else - { - InternalEntry[Metadata] = CurrentValue; - } + InternalEntry[Metadata] = CurrentValue; + InternalEntry.MarkAsTemporary(Metadata, value); } } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs index cda8aa43454..f249a9bd8ec 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs @@ -17,7 +17,8 @@ public void Can_read_and_manipulate_modification_flags() InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Null, InternalEntityEntry.PropertyFlag.Unknown, - InternalEntityEntry.PropertyFlag.IsLoaded); + InternalEntityEntry.PropertyFlag.IsLoaded, + InternalEntityEntry.PropertyFlag.IsTemporary); } } @@ -31,7 +32,8 @@ public void Can_read_and_manipulate_null_flags() InternalEntityEntry.PropertyFlag.Null, InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Unknown, - InternalEntityEntry.PropertyFlag.IsLoaded); + InternalEntityEntry.PropertyFlag.IsLoaded, + InternalEntityEntry.PropertyFlag.IsTemporary); } } @@ -45,7 +47,8 @@ public void Can_read_and_manipulate_not_set_flags() InternalEntityEntry.PropertyFlag.Unknown, InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Null, - InternalEntityEntry.PropertyFlag.IsLoaded); + InternalEntityEntry.PropertyFlag.IsLoaded, + InternalEntityEntry.PropertyFlag.IsTemporary); } } @@ -59,6 +62,22 @@ public void Can_read_and_manipulate_is_loaded_flags() InternalEntityEntry.PropertyFlag.IsLoaded, InternalEntityEntry.PropertyFlag.Modified, InternalEntityEntry.PropertyFlag.Null, + InternalEntityEntry.PropertyFlag.Unknown, + InternalEntityEntry.PropertyFlag.IsTemporary); + } + } + + [ConditionalFact] + public void Can_read_and_manipulate_temporary_flags() + { + for (var i = 0; i < 70; i++) + { + PropertyManipulation( + i, + InternalEntityEntry.PropertyFlag.IsTemporary, + InternalEntityEntry.PropertyFlag.IsLoaded, + InternalEntityEntry.PropertyFlag.Modified, + InternalEntityEntry.PropertyFlag.Null, InternalEntityEntry.PropertyFlag.Unknown); } } @@ -68,7 +87,8 @@ private void PropertyManipulation( InternalEntityEntry.PropertyFlag propertyFlag, InternalEntityEntry.PropertyFlag unusedFlag1, InternalEntityEntry.PropertyFlag unusedFlag2, - InternalEntityEntry.PropertyFlag unusedFlag3) + InternalEntityEntry.PropertyFlag unusedFlag3, + InternalEntityEntry.PropertyFlag unusedFlag4) { var data = new InternalEntityEntry.StateData(propertyCount, propertyCount); @@ -76,6 +96,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag1)); Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); for (var i = 0; i < propertyCount; i++) { @@ -87,12 +108,14 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(j, unusedFlag1)); Assert.False(data.IsPropertyFlagged(j, unusedFlag2)); Assert.False(data.IsPropertyFlagged(j, unusedFlag3)); + Assert.False(data.IsPropertyFlagged(j, unusedFlag4)); } Assert.True(data.AnyPropertiesFlagged(propertyFlag)); Assert.False(data.AnyPropertiesFlagged(unusedFlag1)); Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); } for (var i = 0; i < propertyCount; i++) @@ -105,12 +128,14 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(j, unusedFlag1)); Assert.False(data.IsPropertyFlagged(j, unusedFlag2)); Assert.False(data.IsPropertyFlagged(j, unusedFlag3)); + Assert.False(data.IsPropertyFlagged(j, unusedFlag4)); } Assert.Equal(i < propertyCount - 1, data.AnyPropertiesFlagged(propertyFlag)); Assert.False(data.AnyPropertiesFlagged(unusedFlag1)); Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); } for (var i = 0; i < propertyCount; i++) @@ -119,6 +144,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(i, unusedFlag1)); Assert.False(data.IsPropertyFlagged(i, unusedFlag2)); Assert.False(data.IsPropertyFlagged(i, unusedFlag3)); + Assert.False(data.IsPropertyFlagged(i, unusedFlag4)); } data.FlagAllProperties(propertyCount, propertyFlag, flagged: true); @@ -127,6 +153,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag1)); Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); for (var i = 0; i < propertyCount; i++) { @@ -134,6 +161,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(i, unusedFlag1)); Assert.False(data.IsPropertyFlagged(i, unusedFlag2)); Assert.False(data.IsPropertyFlagged(i, unusedFlag3)); + Assert.False(data.IsPropertyFlagged(i, unusedFlag4)); } data.FlagAllProperties(propertyCount, propertyFlag, flagged: false); @@ -142,6 +170,7 @@ private void PropertyManipulation( Assert.False(data.AnyPropertiesFlagged(unusedFlag1)); Assert.False(data.AnyPropertiesFlagged(unusedFlag2)); Assert.False(data.AnyPropertiesFlagged(unusedFlag3)); + Assert.False(data.AnyPropertiesFlagged(unusedFlag4)); for (var i = 0; i < propertyCount; i++) { @@ -149,6 +178,7 @@ private void PropertyManipulation( Assert.False(data.IsPropertyFlagged(i, unusedFlag1)); Assert.False(data.IsPropertyFlagged(i, unusedFlag2)); Assert.False(data.IsPropertyFlagged(i, unusedFlag3)); + Assert.False(data.IsPropertyFlagged(i, unusedFlag4)); } } diff --git a/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs b/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs new file mode 100644 index 00000000000..b471bb6bb8f --- /dev/null +++ b/test/EFCore.Tests/ChangeTracking/TemporaryValuesTest.cs @@ -0,0 +1,349 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.ValueGeneration; +using Microsoft.EntityFrameworkCore.ValueGeneration.Internal; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking +{ + public class TemporaryValuesTest + { + [ConditionalFact] + public void Set_temporary_values_for_normal_properties() + { + using (var context = new DefaultValuesContext()) + { + var entity = new EntityWithNonIndexers(); + context.Add(entity); + + Assert.Equal(0, entity.ValueProperty); + Assert.Null(entity.NullableValueProperty); + Assert.Null(entity.ReferenceValueProperty); + + Assert.True(context.Entry(entity).Property(e => e.ValueProperty).CurrentValue < 0); + Assert.True(context.Entry(entity).Property(e => e.NullableValueProperty).CurrentValue < 0); + Assert.NotNull(context.Entry(entity).Property(e => e.ReferenceValueProperty).CurrentValue); + + Assert.True(context.Entry(entity).Property(e => e.ValueProperty).IsTemporary); + Assert.True(context.Entry(entity).Property(e => e.NullableValueProperty).IsTemporary); + Assert.True(context.Entry(entity).Property(e => e.ReferenceValueProperty).IsTemporary); + + entity.ValueProperty = 77; + entity.NullableValueProperty = 77; + entity.ReferenceValueProperty = "Seventy Seven"; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(77, entity.ValueProperty); + Assert.Equal(77, entity.NullableValueProperty); + Assert.Equal("Seventy Seven", entity.ReferenceValueProperty); + + Assert.Equal(77, context.Entry(entity).Property(e => e.ValueProperty).CurrentValue); + Assert.Equal(77, context.Entry(entity).Property(e => e.NullableValueProperty).CurrentValue); + Assert.Equal("Seventy Seven", context.Entry(entity).Property(e => e.ReferenceValueProperty).CurrentValue); + + Assert.False(context.Entry(entity).Property(e => e.ValueProperty).IsTemporary); + Assert.False(context.Entry(entity).Property(e => e.NullableValueProperty).IsTemporary); + Assert.False(context.Entry(entity).Property(e => e.ReferenceValueProperty).IsTemporary); + + context.Entry(entity).Property(e => e.ValueProperty).IsTemporary = true; + context.Entry(entity).Property(e => e.NullableValueProperty).IsTemporary = true; + context.Entry(entity).Property(e => e.ReferenceValueProperty).IsTemporary = true; + + Assert.Equal(77, entity.ValueProperty); + Assert.Equal(77, entity.NullableValueProperty); + Assert.Equal("Seventy Seven", entity.ReferenceValueProperty); + + Assert.Equal(77, context.Entry(entity).Property(e => e.ValueProperty).CurrentValue); + Assert.Equal(77, context.Entry(entity).Property(e => e.NullableValueProperty).CurrentValue); + Assert.Equal("Seventy Seven", context.Entry(entity).Property(e => e.ReferenceValueProperty).CurrentValue); + + Assert.True(context.Entry(entity).Property(e => e.ValueProperty).IsTemporary); + Assert.True(context.Entry(entity).Property(e => e.NullableValueProperty).IsTemporary); + Assert.True(context.Entry(entity).Property(e => e.ReferenceValueProperty).IsTemporary); + } + } + + [ConditionalFact] + public void Set_temporary_values_for_indexer_properties() + { + using (var context = new DefaultValuesContext()) + { + var entity1 = new EntityWithIndexerValueProperty(); + var entity2 = new EntityWithIndexerNullableValueProperty(); + var entity3 = new EntityWithIndexerReferenceProperty(); + + context.AddRange(entity1, entity2, entity3); + + Assert.Equal(0, entity1["ValueProperty"]); + Assert.Null(entity2["NullableValueProperty"]); + Assert.Null(entity3["ReferenceValueProperty"]); + + Assert.True(context.Entry(entity1).Property("ValueProperty").CurrentValue < 0); + Assert.True(context.Entry(entity2).Property("NullableValueProperty").CurrentValue < 0); + Assert.NotNull(context.Entry(entity3).Property("ReferenceValueProperty").CurrentValue); + + Assert.True(context.Entry(entity1).Property("ValueProperty").IsTemporary); + Assert.True(context.Entry(entity2).Property("NullableValueProperty").IsTemporary); + Assert.True(context.Entry(entity3).Property("ReferenceValueProperty").IsTemporary); + + entity1["ValueProperty"] = 77; + entity2["NullableValueProperty"] = 77; + entity3["ReferenceValueProperty"] = "Seventy Seven"; + + Assert.Equal(77, entity1["ValueProperty"]); + Assert.Equal(77, entity2["NullableValueProperty"]); + Assert.Equal("Seventy Seven", entity3["ReferenceValueProperty"]); + + Assert.Equal(77, context.Entry(entity1).Property("ValueProperty").CurrentValue); + Assert.Equal(77, context.Entry(entity2).Property("NullableValueProperty").CurrentValue); + Assert.Equal("Seventy Seven", context.Entry(entity3).Property("ReferenceValueProperty").CurrentValue); + + Assert.False(context.Entry(entity1).Property("ValueProperty").IsTemporary); + Assert.False(context.Entry(entity2).Property("NullableValueProperty").IsTemporary); + Assert.False(context.Entry(entity3).Property("ReferenceValueProperty").IsTemporary); + + entity1["ValueProperty"] = 78; + entity2["NullableValueProperty"] = 78; + entity3["ReferenceValueProperty"] = "Seventy Eight"; + + Assert.Equal(78, entity1["ValueProperty"]); + Assert.Equal(78, entity2["NullableValueProperty"]); + Assert.Equal("Seventy Eight", entity3["ReferenceValueProperty"]); + + Assert.Equal(78, context.Entry(entity1).Property("ValueProperty").CurrentValue); + Assert.Equal(78, context.Entry(entity2).Property("NullableValueProperty").CurrentValue); + Assert.Equal("Seventy Eight", context.Entry(entity3).Property("ReferenceValueProperty").CurrentValue); + + Assert.False(context.Entry(entity1).Property("ValueProperty").IsTemporary); + Assert.False(context.Entry(entity2).Property("NullableValueProperty").IsTemporary); + Assert.False(context.Entry(entity3).Property("ReferenceValueProperty").IsTemporary); + + context.Entry(entity1).Property("ValueProperty").IsTemporary = true; + context.Entry(entity2).Property("NullableValueProperty").IsTemporary = true; + context.Entry(entity3).Property("ReferenceValueProperty").IsTemporary = true; + + Assert.Equal(78, entity1["ValueProperty"]); + Assert.Equal(78, entity2["NullableValueProperty"]); + Assert.Equal("Seventy Eight", entity3["ReferenceValueProperty"]); + + Assert.Equal(78, context.Entry(entity1).Property("ValueProperty").CurrentValue); + Assert.Equal(78, context.Entry(entity2).Property("NullableValueProperty").CurrentValue); + Assert.Equal("Seventy Eight", context.Entry(entity3).Property("ReferenceValueProperty").CurrentValue); + + Assert.True(context.Entry(entity1).Property("ValueProperty").IsTemporary); + Assert.True(context.Entry(entity2).Property("NullableValueProperty").IsTemporary); + Assert.True(context.Entry(entity3).Property("ReferenceValueProperty").IsTemporary); + } + } + + [ConditionalFact] + public void Set_temporary_values_for_indexer_properties_types_as_object() + { + using (var context = new DefaultValuesContext()) + { + var entity = new EntityWithIndexersAsObject(); + + context.Add(entity); + + Assert.Null(entity["ValueProperty"]); + Assert.Null(entity["NullableValueProperty"]); + Assert.Null(entity["ReferenceValueProperty"]); + + Assert.True(context.Entry(entity).Property("ValueProperty").CurrentValue < 0); + Assert.True(context.Entry(entity).Property("NullableValueProperty").CurrentValue < 0); + Assert.NotNull(context.Entry(entity).Property("ReferenceValueProperty").CurrentValue); + + Assert.True(context.Entry(entity).Property("ValueProperty").IsTemporary); + Assert.True(context.Entry(entity).Property("NullableValueProperty").IsTemporary); + Assert.True(context.Entry(entity).Property("ReferenceValueProperty").IsTemporary); + + entity["ValueProperty"] = 77; + entity["NullableValueProperty"] = 77; + entity["ReferenceValueProperty"] = "Seventy Seven"; + + Assert.Equal(77, entity["ValueProperty"]); + Assert.Equal(77, entity["NullableValueProperty"]); + Assert.Equal("Seventy Seven", entity["ReferenceValueProperty"]); + + Assert.Equal(77, context.Entry(entity).Property("ValueProperty").CurrentValue); + Assert.Equal(77, context.Entry(entity).Property("NullableValueProperty").CurrentValue); + Assert.Equal("Seventy Seven", context.Entry(entity).Property("ReferenceValueProperty").CurrentValue); + + Assert.False(context.Entry(entity).Property("ValueProperty").IsTemporary); + Assert.False(context.Entry(entity).Property("NullableValueProperty").IsTemporary); + Assert.False(context.Entry(entity).Property("ReferenceValueProperty").IsTemporary); + + entity["ValueProperty"] = 78; + entity["NullableValueProperty"] = 78; + entity["ReferenceValueProperty"] = "Seventy Eight"; + + Assert.Equal(78, entity["ValueProperty"]); + Assert.Equal(78, entity["NullableValueProperty"]); + Assert.Equal("Seventy Eight", entity["ReferenceValueProperty"]); + + Assert.Equal(78, context.Entry(entity).Property("ValueProperty").CurrentValue); + Assert.Equal(78, context.Entry(entity).Property("NullableValueProperty").CurrentValue); + Assert.Equal("Seventy Eight", context.Entry(entity).Property("ReferenceValueProperty").CurrentValue); + + Assert.False(context.Entry(entity).Property("ValueProperty").IsTemporary); + Assert.False(context.Entry(entity).Property("NullableValueProperty").IsTemporary); + Assert.False(context.Entry(entity).Property("ReferenceValueProperty").IsTemporary); + + context.Entry(entity).Property("ValueProperty").IsTemporary = true; + context.Entry(entity).Property("NullableValueProperty").IsTemporary = true; + context.Entry(entity).Property("ReferenceValueProperty").IsTemporary = true; + + Assert.Equal(78, entity["ValueProperty"]); + Assert.Equal(78, entity["NullableValueProperty"]); + Assert.Equal("Seventy Eight", entity["ReferenceValueProperty"]); + + Assert.Equal(78, context.Entry(entity).Property("ValueProperty").CurrentValue); + Assert.Equal(78, context.Entry(entity).Property("NullableValueProperty").CurrentValue); + Assert.Equal("Seventy Eight", context.Entry(entity).Property("ReferenceValueProperty").CurrentValue); + + Assert.True(context.Entry(entity).Property("ValueProperty").IsTemporary); + Assert.True(context.Entry(entity).Property("NullableValueProperty").IsTemporary); + Assert.True(context.Entry(entity).Property("ReferenceValueProperty").IsTemporary); + } + } + + private class EntityWithNonIndexers + { + public int Id { get; set; } + + public int ValueProperty { get; set; } + public int? NullableValueProperty { get; set; } + public string ReferenceValueProperty { get; set; } + } + + private class EntityWithIndexerValueProperty + { + public int Id { get; set; } + + private readonly Dictionary _values = new(); + public int this[string name] + { + get => _values.TryGetValue(name, out var value) ? value : default; + set => _values[name] = value; + } + } + + private class EntityWithIndexerNullableValueProperty + { + public int Id { get; set; } + + private readonly Dictionary _values = new(); + public int? this[string name] + { + get => _values.TryGetValue(name, out var value) ? value : default; + set => _values[name] = value; + } + } + + private class EntityWithIndexerReferenceProperty + { + public int Id { get; set; } + + private readonly Dictionary _values = new(); + public string this[string name] + { + get => _values.TryGetValue(name, out var value) ? value : default; + set => _values[name] = value; + } + } + + private class EntityWithIndexersAsObject + { + public int Id { get; set; } + + private readonly Dictionary _values = new(); + public object this[string name] + { + get => _values.TryGetValue(name, out var value) ? value : default; + set => _values[name] = value; + } + } + + private class DefaultValuesContext : DbContext + { + protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase(GetType().FullName!); + } + + protected internal override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.Property(e => e.ValueProperty) + .ValueGeneratedOnAdd() + .HasValueGenerator(); + + b.Property(e => e.NullableValueProperty) + .ValueGeneratedOnAdd() + .HasValueGenerator(); + + b.Property(e => e.ReferenceValueProperty) + .ValueGeneratedOnAdd() + .HasValueGenerator(); + }); + + modelBuilder.Entity( + b => + { + b.IndexerProperty("ValueProperty") + .ValueGeneratedOnAdd() + .HasValueGenerator(); + + b.IndexerProperty("NullableValueProperty") + .ValueGeneratedOnAdd() + .HasValueGenerator(); + + b.IndexerProperty("ReferenceValueProperty") + .ValueGeneratedOnAdd() + .HasValueGenerator(); + }); + + modelBuilder.Entity( + b => + { + b.IndexerProperty("ValueProperty") + .ValueGeneratedOnAdd() + .HasValueGenerator(); + }); + + modelBuilder.Entity( + b => + { + b.IndexerProperty("NullableValueProperty") + .ValueGeneratedOnAdd() + .HasValueGenerator(); + }); + + modelBuilder.Entity( + b => + { + b.IndexerProperty("ReferenceValueProperty") + .ValueGeneratedOnAdd() + .HasValueGenerator(); + }); + } + + private class TemporaryStringValueGenerator : ValueGenerator + { + public override bool GeneratesTemporaryValues + => true; + + public override string Next(EntityEntry entry) + => Guid.NewGuid().ToString(); + } + } + } +}