Skip to content

Commit d078573

Browse files
authored
Allow property values to be explicitly flagged as temporary (#24620)
This allows values explicitly set by the application as temporary to be stored in and obtained from the entity instance. Fixes #23191 Fixes #24245
1 parent 3be8511 commit d078573

File tree

7 files changed

+449
-38
lines changed

7 files changed

+449
-38
lines changed

Diff for: src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs

+56-5
Original file line numberDiff line numberDiff line change
@@ -552,26 +552,60 @@ public virtual bool IsConceptualNull(IProperty property)
552552
public virtual bool HasTemporaryValue(IProperty property)
553553
=> GetValueType(property) == CurrentValueType.Temporary;
554554

555+
/// <summary>
556+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
557+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
558+
/// any release. You should only use it directly in your code with extreme caution and knowing that
559+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
560+
/// </summary>
561+
public virtual void PropagateValue(
562+
InternalEntityEntry principalEntry,
563+
IProperty principalProperty,
564+
IProperty dependentProperty,
565+
bool isMaterialization = false,
566+
bool setModified = true)
567+
{
568+
var principalValue = principalEntry[principalProperty];
569+
if (principalEntry.HasTemporaryValue(principalProperty))
570+
{
571+
if (principalEntry._stateData.IsPropertyFlagged(principalProperty.GetIndex(), PropertyFlag.IsTemporary))
572+
{
573+
SetProperty(dependentProperty, principalValue, isMaterialization, setModified);
574+
_stateData.FlagProperty(dependentProperty.GetIndex(), PropertyFlag.IsTemporary, true);
575+
}
576+
else
577+
{
578+
SetTemporaryValue(dependentProperty, principalValue);
579+
}
580+
}
581+
else
582+
{
583+
SetProperty(dependentProperty, principalValue, isMaterialization, setModified);
584+
_stateData.FlagProperty(dependentProperty.GetIndex(), PropertyFlag.IsTemporary, false);
585+
}
586+
}
587+
555588
private CurrentValueType GetValueType(
556589
IProperty property,
557590
Func<object?, object?, bool>? equals = null)
558591
{
559-
var tempIndex = property.GetStoreGeneratedIndex();
560-
if (tempIndex == -1)
592+
if (_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary))
561593
{
562-
return CurrentValueType.Normal;
594+
return CurrentValueType.Temporary;
563595
}
564596

565-
if (equals == null)
597+
var tempIndex = property.GetStoreGeneratedIndex();
598+
if (tempIndex == -1)
566599
{
567-
equals = ValuesEqualFunc(property);
600+
return CurrentValueType.Normal;
568601
}
569602

570603
if (!PropertyHasDefaultValue(property))
571604
{
572605
return CurrentValueType.Normal;
573606
}
574607

608+
@equals ??= ValuesEqualFunc(property);
575609
var defaultValue = property.ClrType.GetDefaultValue();
576610
var value = ReadPropertyValue(property);
577611
if (!equals(value, defaultValue))
@@ -611,6 +645,15 @@ public virtual void SetTemporaryValue(IProperty property, object? value, bool se
611645
SetProperty(property, value, isMaterialization: false, setModified, isCascadeDelete: false, CurrentValueType.Temporary);
612646
}
613647

648+
/// <summary>
649+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
650+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
651+
/// any release. You should only use it directly in your code with extreme caution and knowing that
652+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
653+
/// </summary>
654+
public virtual void MarkAsTemporary(IProperty property, bool temporary)
655+
=> _stateData.FlagProperty(property.GetIndex(), PropertyFlag.IsTemporary, temporary);
656+
614657
/// <summary>
615658
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
616659
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -1208,6 +1251,12 @@ private void SetProperty(
12081251

12091252
if (valueType == CurrentValueType.StoreGenerated)
12101253
{
1254+
var defaultValue = asProperty!.ClrType.GetDefaultValue();
1255+
if (!equals(currentValue, defaultValue))
1256+
{
1257+
WritePropertyValue(asProperty, defaultValue, isMaterialization);
1258+
}
1259+
12111260
EnsureStoreGeneratedValues();
12121261
_storeGeneratedValues.SetValue(asProperty!, value, storeGeneratedIndex);
12131262
}
@@ -1285,6 +1334,8 @@ public virtual void AcceptChanges()
12851334
_storeGeneratedValues = new SidecarValues();
12861335
_temporaryValues = new SidecarValues();
12871336
}
1337+
1338+
_stateData.FlagAllProperties(EntityType.PropertyCount(), PropertyFlag.IsTemporary, false);
12881339

12891340
var currentState = EntityState;
12901341
if ((currentState == EntityState.Unchanged)

Diff for: src/EFCore/ChangeTracking/Internal/KeyPropagator.cs

+1-8
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,7 @@ public KeyPropagator(
162162
if (generationProperty == null
163163
|| !principalProperty.ClrType.IsDefaultValue(principalValue))
164164
{
165-
if (principalEntry.HasTemporaryValue(principalProperty))
166-
{
167-
entry.SetTemporaryValue(property, principalValue);
168-
}
169-
else
170-
{
171-
entry[property] = principalValue;
172-
}
165+
entry.PropagateValue(principalEntry, principalProperty, property);
173166

174167
return principalEntry;
175168
}

Diff for: src/EFCore/ChangeTracking/Internal/NavigationFixer.cs

+1-8
Original file line numberDiff line numberDiff line change
@@ -1213,14 +1213,7 @@ private static void SetForeignKeyProperties(
12131213
|| (dependentEntry.IsConceptualNull(dependentProperty)
12141214
&& principalValue != null))
12151215
{
1216-
if (principalEntry.HasTemporaryValue(principalProperty))
1217-
{
1218-
dependentEntry.SetTemporaryValue(dependentProperty, principalValue, setModified);
1219-
}
1220-
else
1221-
{
1222-
dependentEntry.SetProperty(dependentProperty, principalValue, fromQuery, setModified);
1223-
}
1216+
dependentEntry.PropagateValue(principalEntry, principalProperty, dependentProperty, fromQuery, setModified);
12241217

12251218
dependentEntry.StateManager.UpdateDependentMap(dependentEntry, foreignKey);
12261219
dependentEntry.SetRelationshipSnapshotValue(dependentProperty, principalValue);

Diff for: src/EFCore/ChangeTracking/Internal/StateData.cs

+6-5
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,21 @@ internal enum PropertyFlag
1212
Modified = 0,
1313
Null = 1,
1414
Unknown = 2,
15-
IsLoaded = 3
15+
IsLoaded = 3,
16+
IsTemporary = 4
1617
}
1718

1819
internal readonly struct StateData
1920
{
2021
private const int BitsPerInt = 32;
2122
private const int BitsForEntityState = 3;
22-
private const int BitsForEntityFlags = 1;
23-
private const int BitsForPropertyFlags = 4;
23+
private const int BitsForEntityFlags = 5;
24+
private const int BitsForPropertyFlags = 8;
2425
private const int BitsForAdditionalState = BitsForEntityState + BitsForEntityFlags;
2526
private const int EntityStateMask = 0x07;
26-
private const int UnusedStateMask = 0x08; // So entity state uses even number of bits
27+
private const int UnusedStateMask = 0xF8; // So entity state uses even number of bits
2728
private const int AdditionalStateMask = EntityStateMask | UnusedStateMask;
28-
private const int PropertyFlagMask = 0x11111111;
29+
private const int PropertyFlagMask = 0x01010101;
2930

3031
private readonly int[] _bits;
3132

Diff for: src/EFCore/ChangeTracking/PropertyEntry.cs

+2-8
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,8 @@ public virtual bool IsTemporary
6363
get => InternalEntry.HasTemporaryValue(Metadata);
6464
set
6565
{
66-
if (value)
67-
{
68-
InternalEntry.SetTemporaryValue(Metadata, CurrentValue);
69-
}
70-
else
71-
{
72-
InternalEntry[Metadata] = CurrentValue;
73-
}
66+
InternalEntry[Metadata] = CurrentValue;
67+
InternalEntry.MarkAsTemporary(Metadata, value);
7468
}
7569
}
7670

Diff for: test/EFCore.Tests/ChangeTracking/Internal/StateDataTest.cs

+34-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public void Can_read_and_manipulate_modification_flags()
1717
InternalEntityEntry.PropertyFlag.Modified,
1818
InternalEntityEntry.PropertyFlag.Null,
1919
InternalEntityEntry.PropertyFlag.Unknown,
20-
InternalEntityEntry.PropertyFlag.IsLoaded);
20+
InternalEntityEntry.PropertyFlag.IsLoaded,
21+
InternalEntityEntry.PropertyFlag.IsTemporary);
2122
}
2223
}
2324

@@ -31,7 +32,8 @@ public void Can_read_and_manipulate_null_flags()
3132
InternalEntityEntry.PropertyFlag.Null,
3233
InternalEntityEntry.PropertyFlag.Modified,
3334
InternalEntityEntry.PropertyFlag.Unknown,
34-
InternalEntityEntry.PropertyFlag.IsLoaded);
35+
InternalEntityEntry.PropertyFlag.IsLoaded,
36+
InternalEntityEntry.PropertyFlag.IsTemporary);
3537
}
3638
}
3739

@@ -45,7 +47,8 @@ public void Can_read_and_manipulate_not_set_flags()
4547
InternalEntityEntry.PropertyFlag.Unknown,
4648
InternalEntityEntry.PropertyFlag.Modified,
4749
InternalEntityEntry.PropertyFlag.Null,
48-
InternalEntityEntry.PropertyFlag.IsLoaded);
50+
InternalEntityEntry.PropertyFlag.IsLoaded,
51+
InternalEntityEntry.PropertyFlag.IsTemporary);
4952
}
5053
}
5154

@@ -59,6 +62,22 @@ public void Can_read_and_manipulate_is_loaded_flags()
5962
InternalEntityEntry.PropertyFlag.IsLoaded,
6063
InternalEntityEntry.PropertyFlag.Modified,
6164
InternalEntityEntry.PropertyFlag.Null,
65+
InternalEntityEntry.PropertyFlag.Unknown,
66+
InternalEntityEntry.PropertyFlag.IsTemporary);
67+
}
68+
}
69+
70+
[ConditionalFact]
71+
public void Can_read_and_manipulate_temporary_flags()
72+
{
73+
for (var i = 0; i < 70; i++)
74+
{
75+
PropertyManipulation(
76+
i,
77+
InternalEntityEntry.PropertyFlag.IsTemporary,
78+
InternalEntityEntry.PropertyFlag.IsLoaded,
79+
InternalEntityEntry.PropertyFlag.Modified,
80+
InternalEntityEntry.PropertyFlag.Null,
6281
InternalEntityEntry.PropertyFlag.Unknown);
6382
}
6483
}
@@ -68,14 +87,16 @@ private void PropertyManipulation(
6887
InternalEntityEntry.PropertyFlag propertyFlag,
6988
InternalEntityEntry.PropertyFlag unusedFlag1,
7089
InternalEntityEntry.PropertyFlag unusedFlag2,
71-
InternalEntityEntry.PropertyFlag unusedFlag3)
90+
InternalEntityEntry.PropertyFlag unusedFlag3,
91+
InternalEntityEntry.PropertyFlag unusedFlag4)
7292
{
7393
var data = new InternalEntityEntry.StateData(propertyCount, propertyCount);
7494

7595
Assert.False(data.AnyPropertiesFlagged(propertyFlag));
7696
Assert.False(data.AnyPropertiesFlagged(unusedFlag1));
7797
Assert.False(data.AnyPropertiesFlagged(unusedFlag2));
7898
Assert.False(data.AnyPropertiesFlagged(unusedFlag3));
99+
Assert.False(data.AnyPropertiesFlagged(unusedFlag4));
79100

80101
for (var i = 0; i < propertyCount; i++)
81102
{
@@ -87,12 +108,14 @@ private void PropertyManipulation(
87108
Assert.False(data.IsPropertyFlagged(j, unusedFlag1));
88109
Assert.False(data.IsPropertyFlagged(j, unusedFlag2));
89110
Assert.False(data.IsPropertyFlagged(j, unusedFlag3));
111+
Assert.False(data.IsPropertyFlagged(j, unusedFlag4));
90112
}
91113

92114
Assert.True(data.AnyPropertiesFlagged(propertyFlag));
93115
Assert.False(data.AnyPropertiesFlagged(unusedFlag1));
94116
Assert.False(data.AnyPropertiesFlagged(unusedFlag2));
95117
Assert.False(data.AnyPropertiesFlagged(unusedFlag3));
118+
Assert.False(data.AnyPropertiesFlagged(unusedFlag4));
96119
}
97120

98121
for (var i = 0; i < propertyCount; i++)
@@ -105,12 +128,14 @@ private void PropertyManipulation(
105128
Assert.False(data.IsPropertyFlagged(j, unusedFlag1));
106129
Assert.False(data.IsPropertyFlagged(j, unusedFlag2));
107130
Assert.False(data.IsPropertyFlagged(j, unusedFlag3));
131+
Assert.False(data.IsPropertyFlagged(j, unusedFlag4));
108132
}
109133

110134
Assert.Equal(i < propertyCount - 1, data.AnyPropertiesFlagged(propertyFlag));
111135
Assert.False(data.AnyPropertiesFlagged(unusedFlag1));
112136
Assert.False(data.AnyPropertiesFlagged(unusedFlag2));
113137
Assert.False(data.AnyPropertiesFlagged(unusedFlag3));
138+
Assert.False(data.AnyPropertiesFlagged(unusedFlag4));
114139
}
115140

116141
for (var i = 0; i < propertyCount; i++)
@@ -119,6 +144,7 @@ private void PropertyManipulation(
119144
Assert.False(data.IsPropertyFlagged(i, unusedFlag1));
120145
Assert.False(data.IsPropertyFlagged(i, unusedFlag2));
121146
Assert.False(data.IsPropertyFlagged(i, unusedFlag3));
147+
Assert.False(data.IsPropertyFlagged(i, unusedFlag4));
122148
}
123149

124150
data.FlagAllProperties(propertyCount, propertyFlag, flagged: true);
@@ -127,13 +153,15 @@ private void PropertyManipulation(
127153
Assert.False(data.AnyPropertiesFlagged(unusedFlag1));
128154
Assert.False(data.AnyPropertiesFlagged(unusedFlag2));
129155
Assert.False(data.AnyPropertiesFlagged(unusedFlag3));
156+
Assert.False(data.AnyPropertiesFlagged(unusedFlag4));
130157

131158
for (var i = 0; i < propertyCount; i++)
132159
{
133160
Assert.True(data.IsPropertyFlagged(i, propertyFlag));
134161
Assert.False(data.IsPropertyFlagged(i, unusedFlag1));
135162
Assert.False(data.IsPropertyFlagged(i, unusedFlag2));
136163
Assert.False(data.IsPropertyFlagged(i, unusedFlag3));
164+
Assert.False(data.IsPropertyFlagged(i, unusedFlag4));
137165
}
138166

139167
data.FlagAllProperties(propertyCount, propertyFlag, flagged: false);
@@ -142,13 +170,15 @@ private void PropertyManipulation(
142170
Assert.False(data.AnyPropertiesFlagged(unusedFlag1));
143171
Assert.False(data.AnyPropertiesFlagged(unusedFlag2));
144172
Assert.False(data.AnyPropertiesFlagged(unusedFlag3));
173+
Assert.False(data.AnyPropertiesFlagged(unusedFlag4));
145174

146175
for (var i = 0; i < propertyCount; i++)
147176
{
148177
Assert.False(data.IsPropertyFlagged(i, propertyFlag));
149178
Assert.False(data.IsPropertyFlagged(i, unusedFlag1));
150179
Assert.False(data.IsPropertyFlagged(i, unusedFlag2));
151180
Assert.False(data.IsPropertyFlagged(i, unusedFlag3));
181+
Assert.False(data.IsPropertyFlagged(i, unusedFlag4));
152182
}
153183
}
154184

0 commit comments

Comments
 (0)