diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
index 197a53c31b6..1ac722620c3 100644
--- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
+++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs
@@ -237,13 +237,16 @@ private static void TryUniquifyColumnNames(
|| (property.IsConcurrencyToken && otherProperty.IsConcurrencyToken)
|| (!property.Builder.CanSetColumnName(null) && !otherProperty.Builder.CanSetColumnName(null)))
{
+ // Handle this with a default value convention #9329
if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Save
- && otherProperty.GetAfterSaveBehavior() == PropertySaveBehavior.Save
- && property.ValueGenerated is ValueGenerated.Never or ValueGenerated.OnUpdateSometimes
- && otherProperty.ValueGenerated is ValueGenerated.Never or ValueGenerated.OnUpdateSometimes)
+ && property.ValueGenerated is ValueGenerated.Never or ValueGenerated.OnUpdateSometimes)
{
- // Handle this with a default value convention #9329
property.Builder.ValueGenerated(ValueGenerated.OnUpdateSometimes);
+ }
+
+ if (otherProperty.GetAfterSaveBehavior() == PropertySaveBehavior.Save
+ && otherProperty.ValueGenerated is ValueGenerated.Never or ValueGenerated.OnUpdateSometimes)
+ {
otherProperty.Builder.ValueGenerated(ValueGenerated.OnUpdateSometimes);
}
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
index a78aed237c8..313c27fc4bd 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
+++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs
@@ -128,7 +128,7 @@ public static string ConflictingEnlistedTransaction
=> GetString("ConflictingEnlistedTransaction");
///
- /// An instance of entity type '{firstEntityType}' and an instance of entity type '{secondEntityType}' are mapped to the same row, but have different original property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
+ /// Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row, but have different original property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
///
public static string ConflictingOriginalRowValues(object? firstEntityType, object? secondEntityType, object? firstProperty, object? secondProperty, object? column)
=> string.Format(
@@ -136,7 +136,7 @@ public static string ConflictingOriginalRowValues(object? firstEntityType, objec
firstEntityType, secondEntityType, firstProperty, secondProperty, column);
///
- /// Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different original property values {firstConflictingValues} and {secondConflictingValues} for the column '{column}'.
+ /// Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different original property values {firstConflictingValues} and {secondConflictingValues} for the column '{column}'.
///
public static string ConflictingOriginalRowValuesSensitive(object? firstEntityType, object? secondEntityType, object? keyValue, object? firstConflictingValues, object? secondConflictingValues, object? column)
=> string.Format(
@@ -160,7 +160,7 @@ public static string ConflictingRowUpdateTypesSensitive(object? firstEntityType,
firstEntityType, firstKeyValue, firstState, secondEntityType, secondKeyValue, secondState);
///
- /// Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row, but have different property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
+ /// Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row, but have different property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
///
public static string ConflictingRowValues(object? firstEntityType, object? secondEntityType, object? firstProperty, object? secondProperty, object? column)
=> string.Format(
@@ -168,7 +168,7 @@ public static string ConflictingRowValues(object? firstEntityType, object? secon
firstEntityType, secondEntityType, firstProperty, secondProperty, column);
///
- /// Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.
+ /// Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.
///
public static string ConflictingRowValuesSensitive(object? firstEntityType, object? secondEntityType, object? keyValue, object? firstConflictingValue, object? secondConflictingValue, object? column)
=> string.Format(
@@ -1034,7 +1034,7 @@ public static string JsonEntityMappedToDifferentViewThanOwner(object? jsonType,
jsonType, viewName, ownerType, ownerViewName);
///
- /// Multiple owned root entities are mapped to the same JSON column '{column}' in table '{table}'. Each owned root entity must map to a different column.
+ /// JSON entity '{jsonEntity}' is missing key information. This is not allowed for tracking queries since EF can't correctly build identity for this entity object.
///
public static string JsonEntityMissingKeyInformation(object? jsonEntity)
=> string.Format(
@@ -1155,6 +1155,12 @@ public static string JsonNodeMustBeHandledByProviderSpecificVisitor
public static string JsonPropertyNameShouldBeConfiguredOnNestedNavigation
=> GetString("JsonPropertyNameShouldBeConfiguredOnNestedNavigation");
+ ///
+ /// Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
+ ///
+ public static string JsonQueryLinqOperatorsNotSupported
+ => GetString("JsonQueryLinqOperatorsNotSupported");
+
///
/// Invalid token type: '{tokenType}'.
///
@@ -1163,12 +1169,6 @@ public static string JsonReaderInvalidTokenType(object? tokenType)
GetString("JsonReaderInvalidTokenType", nameof(tokenType)),
tokenType);
- ///
- /// Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
- ///
- public static string JsonQueryLinqOperatorsNotSupported
- => GetString("JsonQueryLinqOperatorsNotSupported");
-
///
/// Entity {entity} is required but the JSON element containing it is null.
///
@@ -1544,12 +1544,12 @@ public static string SetOperationsOnDifferentStoreTypes
=> GetString("SetOperationsOnDifferentStoreTypes");
///
- /// A set operation 'setOperationType' requires valid type mapping for at least one of its sides.
+ /// A set operation '{setOperationType}' requires valid type mapping for at least one of its sides.
///
public static string SetOperationsRequireAtLeastOneSideWithValidTypeMapping(object? setOperationType)
- => string.Format(
- GetString("SetOperationsRequireAtLeastOneSideWithValidTypeMapping", nameof(setOperationType)),
- setOperationType);
+ => string.Format(
+ GetString("SetOperationsRequireAtLeastOneSideWithValidTypeMapping", nameof(setOperationType)),
+ setOperationType);
///
/// The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method.
@@ -2038,7 +2038,7 @@ public static string UnsupportedOperatorForSqlExpression(object? nodeType, objec
nodeType, expressionType);
///
- /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'.
+ /// No relational type mapping can be found for property '{entity}.{property}' and the current provider doesn't specify a default store type for the properties of type '{clrType}'.
///
public static string UnsupportedPropertyType(object? entity, object? property, object? clrType)
=> string.Format(
diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx
index 6ca3e9b0d28..81522992c44 100644
--- a/src/EFCore.Relational/Properties/RelationalStrings.resx
+++ b/src/EFCore.Relational/Properties/RelationalStrings.resx
@@ -161,10 +161,10 @@
The connection is currently enlisted in a transaction. The enlisted transaction needs to be completed before starting a new transaction.
- An instance of entity type '{firstEntityType}' and an instance of entity type '{secondEntityType}' are mapped to the same row, but have different original property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
+ Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row, but have different original property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
- Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different original property values {firstConflictingValues} and {secondConflictingValues} for the column '{column}'.
+ Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different original property values {firstConflictingValues} and {secondConflictingValues} for the column '{column}'.
An instance of entity type '{firstEntityType}' is marked as '{firstState}', but an instance of entity type '{secondEntityType}' is marked as '{secondState}' and both are mapped to the same row. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the key values.
@@ -173,10 +173,10 @@
The instance of entity type '{firstEntityType}' with the key value '{firstKeyValue}' is marked as '{firstState}', but the instance of entity type '{secondEntityType}' with the key value '{secondKeyValue}' is marked as '{secondState}' and both are mapped to the same row.
- Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row, but have different property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
+ Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row, but have different property values for the properties {firstProperty} and {secondProperty} mapped to '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
- Instances of entity types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.
+ Instances of types '{firstEntityType}' and '{secondEntityType}' are mapped to the same row with the key value '{keyValue}', but have different property values '{firstConflictingValue}' and '{secondConflictingValue}' for the column '{column}'.
A seed entity for entity type '{entityType}' has the same key value as another seed entity mapped to the same table '{table}', but have different values for the column '{column}'. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting values.
@@ -553,12 +553,12 @@
The JSON property name should only be configured on nested owned navigations.
-
- Invalid token type: '{tokenType}'.
-
Composing LINQ operators over collections inside JSON documents isn't supported or hasn't been implemented by your EF provider.
+
+ Invalid token type: '{tokenType}'.
+
Entity {entity} is required but the JSON element containing it is null.
@@ -1008,7 +1008,7 @@
Unable to translate set operation when matching columns on both sides have different store types.
- A set operation 'setOperationType' requires valid type mapping for at least one of its sides.
+ A set operation '{setOperationType}' requires valid type mapping for at least one of its sides.
The SetProperty<TProperty> method can only be used within 'ExecuteUpdate' method.
@@ -1235,4 +1235,4 @@
'VisitChildren' must be overridden in the class deriving from 'SqlExpression'.
-
+
\ No newline at end of file
diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs
index 594c177d64a..0cd07ac864a 100644
--- a/src/EFCore.Relational/Update/ColumnModification.cs
+++ b/src/EFCore.Relational/Update/ColumnModification.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Data;
+using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
namespace Microsoft.EntityFrameworkCore.Update;
@@ -127,8 +128,8 @@ public virtual object? OriginalValue
get => Entry == null
? _originalValue
: Entry.SharedIdentityEntry == null
- ? Entry.GetOriginalValue(Property!)
- : Entry.SharedIdentityEntry.GetOriginalValue(Property!);
+ ? GetOriginalValue(Entry, Property!)
+ : GetOriginalValue(Entry.SharedIdentityEntry, Property!);
set
{
if (Entry == null)
@@ -137,7 +138,7 @@ public virtual object? OriginalValue
}
else
{
- Entry.SetOriginalValue(Property!, value);
+ SetOriginalValue(value);
if (_sharedColumnModifications != null)
{
foreach (var sharedModification in _sharedColumnModifications)
@@ -156,7 +157,7 @@ public virtual object? Value
? _value
: Entry.EntityState == EntityState.Deleted
? null
- : Entry.GetCurrentValue(Property!);
+ : GetCurrentValue(Entry, Property!);
set
{
if (Entry == null)
@@ -165,7 +166,7 @@ public virtual object? Value
}
else
{
- Entry.SetStoreGeneratedValue(Property!, value);
+ SetStoreGeneratedValue(Entry, Property!, value);
if (_sharedColumnModifications != null)
{
foreach (var sharedModification in _sharedColumnModifications)
@@ -177,6 +178,91 @@ public virtual object? Value
}
}
+#pragma warning disable EF1001 // Internal EF Core API usage.
+ ///
+ /// 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 static object? GetOriginalValue(IUpdateEntry entry, IProperty property)
+ => (entry.SharedIdentityEntry == null
+ ? GetEntry((IInternalEntry)entry, property)
+ : GetEntry((IInternalEntry)entry.SharedIdentityEntry, property))
+ .GetOriginalValue(property);
+
+ ///
+ /// 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 static object? GetOriginalProviderValue(IUpdateEntry entry, IProperty property)
+ => (entry.SharedIdentityEntry == null
+ ? GetEntry((IInternalEntry)entry, property)
+ : GetEntry((IInternalEntry)entry.SharedIdentityEntry, property))
+ .GetOriginalProviderValue(property);
+
+ private void SetOriginalValue(object? value)
+ => GetEntry((IInternalEntry)Entry!, Property!).SetOriginalValue(Property!, value);
+
+ ///
+ /// 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 static object? GetCurrentValue(IUpdateEntry entry, IProperty property)
+ => GetEntry((IInternalEntry)entry, property).GetCurrentValue(property);
+
+ ///
+ /// 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 static object? GetCurrentProviderValue(IUpdateEntry entry, IProperty property)
+ => GetEntry((IInternalEntry)entry, property).GetCurrentProviderValue(property);
+
+ ///
+ /// 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 static void SetStoreGeneratedValue(IUpdateEntry entry, IProperty property, object? value)
+ => GetEntry((IInternalEntry)entry, property).SetStoreGeneratedValue(property, value);
+
+ ///
+ /// 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 static bool IsModified(IUpdateEntry entry, IProperty property)
+ => GetEntry((IInternalEntry)entry, property).IsModified(property);
+
+ ///
+ /// 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 static bool IsStoreGenerated(IUpdateEntry entry, IProperty property)
+ => GetEntry((IInternalEntry)entry, property).IsStoreGenerated(property);
+
+ private static IInternalEntry GetEntry(IInternalEntry entry, IPropertyBase property)
+ {
+ if (property.DeclaringType.IsAssignableFrom(entry.StructuralType))
+ {
+ return entry;
+ }
+
+ var complexProperty = ((IComplexType)property.DeclaringType).ComplexProperty;
+ return GetEntry(entry, complexProperty).GetComplexPropertyEntry(complexProperty);
+ }
+#pragma warning restore EF1001 // Internal EF Core API usage.
+
///
public virtual string? JsonPath { get; }
@@ -192,47 +278,50 @@ public virtual void AddSharedColumnModification(IColumnModification modification
if (UseCurrentValueParameter
&& !Property.GetProviderValueComparer().Equals(
- Entry.GetCurrentProviderValue(Property),
- modification.Entry.GetCurrentProviderValue(modification.Property)))
+ GetCurrentProviderValue(Entry, Property),
+ GetCurrentProviderValue(modification.Entry, modification.Property)))
{
+#pragma warning disable EF1001 // Internal EF Core API usage.
+ var existingEntry = GetEntry((IInternalEntry)Entry!, Property);
+ var newEntry = GetEntry((IInternalEntry)modification.Entry, modification.Property);
+
if (_sensitiveLoggingEnabled)
{
throw new InvalidOperationException(
RelationalStrings.ConflictingRowValuesSensitive(
- Entry.EntityType.DisplayName(),
- modification.Entry!.EntityType.DisplayName(),
+ existingEntry.StructuralType.DisplayName(),
+ newEntry.StructuralType.DisplayName(),
Entry.BuildCurrentValuesString(Entry.EntityType.FindPrimaryKey()!.Properties),
- Entry.BuildCurrentValuesString(new[] { Property }),
- modification.Entry.BuildCurrentValuesString(new[] { modification.Property }),
+ GetEntry((IInternalEntry)Entry!, Property).BuildCurrentValuesString(new[] { Property }),
+ newEntry.BuildCurrentValuesString(new[] { modification.Property }),
ColumnName));
}
throw new InvalidOperationException(
RelationalStrings.ConflictingRowValues(
- Entry.EntityType.DisplayName(),
- modification.Entry.EntityType.DisplayName(),
+ existingEntry.StructuralType.DisplayName(),
+ newEntry.StructuralType.DisplayName(),
new[] { Property }.Format(),
new[] { modification.Property }.Format(),
ColumnName));
+#pragma warning restore EF1001 // Internal EF Core API usage.
}
- if (UseOriginalValueParameter
- && !Property.GetProviderValueComparer().Equals(
- Entry.SharedIdentityEntry == null
- ? Entry.GetOriginalProviderValue(Property)
- : Entry.SharedIdentityEntry.GetOriginalProviderValue(Property),
- modification.Entry.SharedIdentityEntry == null
- ? modification.Entry.GetOriginalProviderValue(modification.Property)
- : modification.Entry.SharedIdentityEntry.GetOriginalProviderValue(modification.Property)))
+ if (UseOriginalValueParameter)
{
+ var originalValue = GetOriginalProviderValue(Entry, Property);
+ if (Property.GetProviderValueComparer().Equals(
+ originalValue,
+ GetOriginalProviderValue(modification.Entry, modification.Property)))
+ {
+ _sharedColumnModifications.Add(modification);
+ return;
+ }
+
if (Entry.EntityState == EntityState.Modified
&& modification.Entry.EntityState == EntityState.Added
&& modification.Entry.SharedIdentityEntry == null)
{
- var originalValue = Entry.SharedIdentityEntry == null
- ? Entry.GetOriginalProviderValue(Property)
- : Entry.SharedIdentityEntry.GetOriginalProviderValue(Property);
-
var typeMapping = modification.Property.GetTypeMapping();
var converter = typeMapping.Converter;
if (converter != null)
@@ -244,25 +333,29 @@ public virtual void AddSharedColumnModification(IColumnModification modification
}
else
{
+#pragma warning disable EF1001 // Internal EF Core API usage.
+ var existingEntry = GetEntry((IInternalEntry)Entry!, Property);
+ var newEntry = GetEntry((IInternalEntry)modification.Entry, modification.Property);
if (_sensitiveLoggingEnabled)
{
throw new InvalidOperationException(
RelationalStrings.ConflictingOriginalRowValuesSensitive(
- Entry.EntityType.DisplayName(),
- modification.Entry.EntityType.DisplayName(),
+ existingEntry.StructuralType.DisplayName(),
+ newEntry.StructuralType.DisplayName(),
Entry.BuildCurrentValuesString(Entry.EntityType.FindPrimaryKey()!.Properties),
- Entry.BuildOriginalValuesString(new[] { Property }),
- modification.Entry.BuildOriginalValuesString(new[] { modification.Property }),
+ existingEntry.BuildOriginalValuesString(new[] { Property }),
+ newEntry.BuildOriginalValuesString(new[] { modification.Property }),
ColumnName));
}
throw new InvalidOperationException(
RelationalStrings.ConflictingOriginalRowValues(
- Entry.EntityType.DisplayName(),
- modification.Entry.EntityType.DisplayName(),
+ existingEntry.StructuralType.DisplayName(),
+ newEntry.StructuralType.DisplayName(),
new[] { Property }.Format(),
new[] { modification.Property }.Format(),
ColumnName));
+#pragma warning restore EF1001 // Internal EF Core API usage.
}
}
diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs
index d54de736ace..6fc8bfd2fed 100644
--- a/src/EFCore.Relational/Update/ModificationCommand.cs
+++ b/src/EFCore.Relational/Update/ModificationCommand.cs
@@ -5,8 +5,10 @@
using System.Data;
using System.Text;
using System.Text.Json;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using IColumnMapping = Microsoft.EntityFrameworkCore.Metadata.IColumnMapping;
using ITableMapping = Microsoft.EntityFrameworkCore.Metadata.ITableMapping;
@@ -249,18 +251,21 @@ private List GenerateColumnModifications()
var state = EntityState;
var adding = state == EntityState.Added;
var updating = state == EntityState.Modified;
+ var deleting = state == EntityState.Deleted;
var columnModifications = new List();
Dictionary? sharedTableColumnMap = null;
var jsonEntry = false;
if (_entries.Count > 1
- || _entries is [{ SharedIdentityEntry: not null }])
+ || _entries is [{ SharedIdentityEntry: not null }]
+ || _entries[0].EntityType.GetComplexProperties().Count() > 0)
{
Check.DebugAssert(StoreStoredProcedure is null, "Multiple entries/shared identity not supported with stored procedures");
sharedTableColumnMap = new Dictionary();
- if (_comparer != null)
+ if (_comparer != null
+ && _entries.Count > 1)
{
_entries.Sort(_comparer);
}
@@ -280,11 +285,11 @@ private List GenerateColumnModifications()
: tableMapping;
if (sharedTableMapping != null)
{
- InitializeSharedColumns(entry.SharedIdentityEntry, sharedTableMapping, updating, sharedTableColumnMap);
+ HandleSharedColumns(entry.SharedIdentityEntry.EntityType, entry.SharedIdentityEntry, sharedTableMapping, deleting, sharedTableColumnMap);
}
}
- InitializeSharedColumns(entry, tableMapping, updating, sharedTableColumnMap);
+ HandleSharedColumns(entry.EntityType, entry, tableMapping, deleting, sharedTableColumnMap);
if (!jsonEntry && entry.EntityType.IsMappedToJson())
{
@@ -295,115 +300,7 @@ private List GenerateColumnModifications()
if (jsonEntry)
{
- var jsonColumnsUpdateMap = new Dictionary();
- var processedEntries = new List();
- foreach (var entry in _entries.Where(e => e.EntityType.IsMappedToJson()))
- {
- var jsonColumn = GetTableMapping(entry.EntityType)!.Table.FindColumn(entry.EntityType.GetContainerColumnName()!)!;
- var jsonPartialUpdateInfo = FindJsonPartialUpdateInfo(entry, processedEntries);
-
- if (jsonPartialUpdateInfo == null)
- {
- continue;
- }
-
- if (jsonColumnsUpdateMap.TryGetValue(jsonColumn, out var currentJsonPartialUpdateInfo))
- {
- jsonPartialUpdateInfo = FindCommonJsonPartialUpdateInfo(
- currentJsonPartialUpdateInfo,
- jsonPartialUpdateInfo);
- }
-
- jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo;
- }
-
- foreach (var (jsonColumn, updateInfo) in jsonColumnsUpdateMap)
- {
- var finalUpdatePathElement = updateInfo.Path.Last();
- var navigation = finalUpdatePathElement.Navigation;
- var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping;
- var navigationValue = finalUpdatePathElement.ParentEntry.GetCurrentValue(navigation);
- var jsonPathString = string.Join(
- ".", updateInfo.Path.Select(x => x.PropertyName + (x.Ordinal != null ? "[" + x.Ordinal + "]" : "")));
- if (updateInfo.Property is IProperty property)
- {
- var columnModificationParameters = new ColumnModificationParameters(
- jsonColumn.Name,
- value: updateInfo.PropertyValue,
- property: property,
- columnType: jsonColumnTypeMapping.StoreType,
- jsonColumnTypeMapping,
- jsonPath: jsonPathString + "." + updateInfo.Property.GetJsonPropertyName(),
- read: false,
- write: true,
- key: false,
- condition: false,
- _sensitiveLoggingEnabled) { GenerateParameterName = _generateParameterName };
-
- ProcessSinglePropertyJsonUpdate(ref columnModificationParameters);
-
- columnModifications.Add(new ColumnModification(columnModificationParameters));
- }
- else
- {
- var stream = new MemoryStream();
- var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false });
- if (finalUpdatePathElement.Ordinal != null && navigationValue != null)
- {
- var i = 0;
- foreach (var navigationValueElement in (IEnumerable)navigationValue)
- {
- if (i == finalUpdatePathElement.Ordinal)
- {
- WriteJson(
- writer,
- navigationValueElement,
- finalUpdatePathElement.ParentEntry,
- navigation.TargetEntityType,
- ordinal: null,
- isCollection: false,
- isTopLevel: true);
-
- break;
- }
-
- i++;
- }
- }
- else
- {
- WriteJson(
- writer,
- navigationValue,
- finalUpdatePathElement.ParentEntry,
- navigation.TargetEntityType,
- ordinal: null,
- isCollection: navigation.IsCollection,
- isTopLevel: true);
- }
-
- writer.Flush();
-
- var value = writer.BytesCommitted > 0
- ? Encoding.UTF8.GetString(stream.ToArray())
- : null;
-
- columnModifications.Add(
- new ColumnModification(
- new ColumnModificationParameters(
- jsonColumn.Name,
- value: value,
- property: updateInfo.Property,
- columnType: jsonColumnTypeMapping.StoreType,
- jsonColumnTypeMapping,
- jsonPath: jsonPathString,
- read: false,
- write: true,
- key: false,
- condition: false,
- _sensitiveLoggingEnabled) { GenerateParameterName = _generateParameterName }));
- }
- }
+ HandleJson(columnModifications);
}
foreach (var entry in _entries.Where(x => !x.EntityType.IsMappedToJson()))
@@ -425,10 +322,7 @@ entry.EntityState is EntityState.Modified or EntityState.Added
&& tableMapping.Table.IsOptional(entry.EntityType)
&& tableMapping.Table.GetRowInternalForeignKeys(entry.EntityType).Any();
- foreach (var columnMapping in tableMapping.ColumnMappings)
- {
- HandleColumnModification(columnMapping);
- }
+ HandleNonJson(entry.EntityType);
}
else // Stored procedure mapping case
{
@@ -535,6 +429,20 @@ entry.EntityState is EntityState.Modified or EntityState.Added
}
}
+ void HandleNonJson(ITypeBase structuralType)
+ {
+ var tableMapping = GetTableMapping(structuralType)!;
+ foreach (var columnMapping in tableMapping.ColumnMappings)
+ {
+ HandleColumnModification(columnMapping);
+ }
+
+ foreach (var complexProperty in structuralType.GetComplexProperties())
+ {
+ HandleNonJson(complexProperty.ComplexType);
+ }
+ }
+
void HandleColumnModification(IColumnMappingBase columnMapping)
{
var property = columnMapping.Property;
@@ -551,7 +459,7 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
// Store-generated properties generally need to be read back (unless we're deleting).
// One exception is if the property is mapped to a non-output parameter.
var readValue = state != EntityState.Deleted
- && entry.IsStoreGenerated(property)
+ && ColumnModification.IsStoreGenerated(entry, property)
&& (storedProcedureParameter is null || storedProcedureParameter.Direction.HasFlag(ParameterDirection.Output));
ColumnValuePropagator? columnPropagator = null;
@@ -563,6 +471,7 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
if (adding)
{
writeValue = property.GetBeforeSaveBehavior() == PropertySaveBehavior.Save;
+ columnPropagator?.TryPropagate(columnMapping, entry);
}
else if (((updating && property.GetAfterSaveBehavior() == PropertySaveBehavior.Save)
|| (!isKey && nonMainEntry)
@@ -572,7 +481,7 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
// Note that for stored procedures we always need to send all parameters, regardless of whether the property
// actually changed.
writeValue = columnPropagator?.TryPropagate(columnMapping, entry)
- ?? (entry.EntityState == EntityState.Added || entry.IsModified(property) || StoreStoredProcedure is not null);
+ ?? (entry.EntityState == EntityState.Added || ColumnModification.IsModified(entry, property) || StoreStoredProcedure is not null);
}
}
@@ -619,6 +528,7 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
}
else if (optionalDependentWithAllNull
&& state == EntityState.Modified
+ && property.DeclaringType == entry.EntityType
&& entry.GetCurrentValue(property) is not null)
{
optionalDependentWithAllNull = false;
@@ -628,6 +538,22 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
return columnModifications;
+ void HandleSharedColumns(
+ ITypeBase structuralType,
+ IUpdateEntry entry,
+ ITableMapping tableMapping,
+ bool deleting,
+ Dictionary sharedTableColumnMap)
+ {
+ InitializeSharedColumns(entry, tableMapping, deleting, sharedTableColumnMap);
+
+ foreach (var complexProperty in structuralType.GetComplexProperties())
+ {
+ HandleSharedColumns(
+ complexProperty.ComplexType, entry, GetTableMapping(complexProperty.ComplexType)!, deleting, sharedTableColumnMap);
+ }
+ }
+
static JsonPartialUpdateInfo? FindJsonPartialUpdateInfo(IUpdateEntry entry, List processedEntries)
{
var result = new JsonPartialUpdateInfo();
@@ -667,10 +593,10 @@ void HandleColumnModification(IColumnMappingBase columnMapping)
result.Path.Insert(0, pathEntry);
}
- var modifiedMembers = entry.ToEntityEntry().Properties.Where(m => m.IsModified).ToList();
+ var modifiedMembers = entry.EntityType.GetProperties().Where(entry.IsModified).ToList();
if (modifiedMembers.Count == 1)
{
- result.Property = modifiedMembers.Single().Metadata;
+ result.Property = modifiedMembers[0];
result.PropertyValue = entry.GetCurrentValue(result.Property);
}
else
@@ -720,6 +646,121 @@ static JsonPartialUpdateInfo FindCommonJsonPartialUpdateInfo(
return result;
}
+
+ void HandleJson(List columnModifications)
+ {
+ var jsonColumnsUpdateMap = new Dictionary();
+ var processedEntries = new List();
+ foreach (var entry in _entries.Where(e => e.EntityType.IsMappedToJson()))
+ {
+ var jsonColumn = GetTableMapping(entry.EntityType)!.Table.FindColumn(entry.EntityType.GetContainerColumnName()!)!;
+ var jsonPartialUpdateInfo = FindJsonPartialUpdateInfo(entry, processedEntries);
+
+ if (jsonPartialUpdateInfo == null)
+ {
+ continue;
+ }
+
+ if (jsonColumnsUpdateMap.TryGetValue(jsonColumn, out var currentJsonPartialUpdateInfo))
+ {
+ jsonPartialUpdateInfo = FindCommonJsonPartialUpdateInfo(
+ currentJsonPartialUpdateInfo,
+ jsonPartialUpdateInfo);
+ }
+
+ jsonColumnsUpdateMap[jsonColumn] = jsonPartialUpdateInfo;
+ }
+
+ foreach (var (jsonColumn, updateInfo) in jsonColumnsUpdateMap)
+ {
+ var finalUpdatePathElement = updateInfo.Path.Last();
+ var navigation = finalUpdatePathElement.Navigation;
+ var jsonColumnTypeMapping = jsonColumn.StoreTypeMapping;
+ var navigationValue = finalUpdatePathElement.ParentEntry.GetCurrentValue(navigation);
+ var jsonPathString = string.Join(
+ ".", updateInfo.Path.Select(x => x.PropertyName + (x.Ordinal != null ? "[" + x.Ordinal + "]" : "")));
+ if (updateInfo.Property is IProperty property)
+ {
+ var columnModificationParameters = new ColumnModificationParameters(
+ jsonColumn.Name,
+ value: updateInfo.PropertyValue,
+ property: property,
+ columnType: jsonColumnTypeMapping.StoreType,
+ jsonColumnTypeMapping,
+ jsonPath: jsonPathString + "." + updateInfo.Property.GetJsonPropertyName(),
+ read: false,
+ write: true,
+ key: false,
+ condition: false,
+ _sensitiveLoggingEnabled)
+ { GenerateParameterName = _generateParameterName };
+
+ ProcessSinglePropertyJsonUpdate(ref columnModificationParameters);
+
+ columnModifications.Add(new ColumnModification(columnModificationParameters));
+ }
+ else
+ {
+ var stream = new MemoryStream();
+ var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false });
+ if (finalUpdatePathElement.Ordinal != null && navigationValue != null)
+ {
+ var i = 0;
+ foreach (var navigationValueElement in (IEnumerable)navigationValue)
+ {
+ if (i == finalUpdatePathElement.Ordinal)
+ {
+ WriteJson(
+ writer,
+ navigationValueElement,
+ finalUpdatePathElement.ParentEntry,
+ navigation.TargetEntityType,
+ ordinal: null,
+ isCollection: false,
+ isTopLevel: true);
+
+ break;
+ }
+
+ i++;
+ }
+ }
+ else
+ {
+ WriteJson(
+ writer,
+ navigationValue,
+ finalUpdatePathElement.ParentEntry,
+ navigation.TargetEntityType,
+ ordinal: null,
+ isCollection: navigation.IsCollection,
+ isTopLevel: true);
+ }
+
+ writer.Flush();
+
+ var value = writer.BytesCommitted > 0
+ ? Encoding.UTF8.GetString(stream.ToArray())
+ : null;
+
+ columnModifications.Add(
+ new ColumnModification(
+ new ColumnModificationParameters(
+ jsonColumn.Name,
+ value: value,
+ property: updateInfo.Property,
+ columnType: jsonColumnTypeMapping.StoreType,
+ jsonColumnTypeMapping,
+ jsonPath: jsonPathString,
+ read: false,
+ write: true,
+ key: false,
+ condition: false,
+ _sensitiveLoggingEnabled)
+ { GenerateParameterName = _generateParameterName }));
+ }
+ }
+ }
}
///
@@ -854,9 +895,9 @@ private void WriteJson(
writer.WriteEndObject();
}
- private ITableMapping? GetTableMapping(IEntityType entityType)
+ private ITableMapping? GetTableMapping(ITypeBase structuralType)
{
- foreach (var mapping in entityType.GetTableMappings())
+ foreach (var mapping in structuralType.GetTableMappings())
{
var table = mapping.Table;
if (table.Name == TableName
@@ -894,7 +935,7 @@ private void WriteJson(
private static void InitializeSharedColumns(
IUpdateEntry entry,
ITableMapping tableMapping,
- bool updating,
+ bool deleting,
Dictionary columnMap)
{
foreach (var columnMapping in tableMapping.ColumnMappings)
@@ -904,6 +945,12 @@ private static void InitializeSharedColumns(
continue;
}
+ if (columnMapping.Column.PropertyMappings.Count == 1
+ && entry.SharedIdentityEntry == null)
+ {
+ continue;
+ }
+
var columnName = columnMapping.Column.Name;
if (!columnMap.TryGetValue(columnName, out var columnPropagator))
{
@@ -911,7 +958,7 @@ private static void InitializeSharedColumns(
columnMap.Add(columnName, columnPropagator);
}
- if (updating)
+ if (!deleting)
{
columnPropagator.RecordValue(columnMapping, entry);
}
@@ -1017,11 +1064,12 @@ public override string ToString()
return result;
}
- private sealed class ColumnValuePropagator
+ private class ColumnValuePropagator
{
private bool _write;
private object? _originalValue;
private object? _currentValue;
+ private bool _originalValueInitialized;
public IColumnModification? ColumnModification { get; set; }
@@ -1032,20 +1080,30 @@ public void RecordValue(IColumnMapping mapping, IUpdateEntry entry)
{
case EntityState.Modified:
if (!_write
- && entry.IsModified(property))
+ && Update.ColumnModification.IsModified(entry, property))
{
_write = true;
- _currentValue = entry.GetCurrentProviderValue(property);
+ _currentValue = Update.ColumnModification.GetCurrentProviderValue(entry, property);
+ _originalValue = Update.ColumnModification.GetOriginalProviderValue(entry, property);
+ _originalValueInitialized = true;
}
break;
case EntityState.Added:
- _currentValue = entry.GetCurrentProviderValue(property);
- _write = !mapping.Column.ProviderValueComparer.Equals(_originalValue, _currentValue);
+ if (!property.GetValueComparer().Equals(
+ Update.ColumnModification.GetCurrentValue(entry, property),
+ property.Sentinel))
+ {
+ _currentValue = Update.ColumnModification.GetCurrentProviderValue(entry, property);
+ }
+
+ _write = !_originalValueInitialized
+ || !mapping.Column.ProviderValueComparer.Equals(_originalValue, _currentValue);
break;
case EntityState.Deleted:
- _originalValue = entry.GetOriginalProviderValue(property);
+ _originalValue = Update.ColumnModification.GetOriginalProviderValue(entry, property);
+ _originalValueInitialized = true;
if (!_write
&& !property.IsPrimaryKey())
{
@@ -1062,9 +1120,16 @@ public bool TryPropagate(IColumnMappingBase mapping, IUpdateEntry entry)
var property = mapping.Property;
if (_write
&& (entry.EntityState == EntityState.Unchanged
- || (entry.EntityState == EntityState.Modified && !entry.IsModified(property))
+ || (entry.EntityState == EntityState.Modified && !Update.ColumnModification.IsModified(entry, property))
|| (entry.EntityState == EntityState.Added
- && mapping.Column.ProviderValueComparer.Equals(_originalValue, entry.GetCurrentProviderValue(property)))))
+ && ((!_originalValueInitialized
+ && (property.GetValueComparer().Equals(
+ Update.ColumnModification.GetCurrentValue(entry, property),
+ property.Sentinel)))
+ || (_originalValueInitialized
+ && mapping.Column.ProviderValueComparer.Equals(
+ Update.ColumnModification.GetCurrentProviderValue(entry, property),
+ _originalValue))))))
{
if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Save
|| entry.EntityState == EntityState.Added)
@@ -1076,7 +1141,7 @@ public bool TryPropagate(IColumnMappingBase mapping, IUpdateEntry entry)
value = converter.ConvertFromProvider(value);
}
- entry.SetStoreGeneratedValue(property, value);
+ Update.ColumnModification.SetStoreGeneratedValue(entry, property, value);
}
return false;
diff --git a/src/EFCore/ChangeTracking/Internal/ComplexEntries.cs b/src/EFCore/ChangeTracking/Internal/ComplexEntries.cs
new file mode 100644
index 00000000000..84035910e55
--- /dev/null
+++ b/src/EFCore/ChangeTracking/Internal/ComplexEntries.cs
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+
+namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+
+public sealed partial class InternalEntityEntry
+{
+ private readonly struct ComplexEntries : IEnumerable
+ {
+ private readonly InternalComplexEntry?[] _entries;
+
+ public ComplexEntries(IInternalEntry entry)
+ {
+ _entries = new InternalComplexEntry[entry.StructuralType.ComplexPropertyCount];
+ }
+
+ public InternalComplexEntry GetEntry(IInternalEntry entry, IComplexProperty property)
+ {
+ var index = property.GetIndex();
+
+ Check.DebugAssert(index != -1 && index < _entries.Length, "Invalid index on complex property " + property.Name);
+ Check.DebugAssert(!IsEmpty, "Complex entries are empty");
+
+ var complexEntry = _entries[index];
+ if (complexEntry == null)
+ {
+ complexEntry = new InternalComplexEntry(entry.StateManager, property.ComplexType, entry, entry[property]);
+ _entries[index] = complexEntry;
+ }
+ return complexEntry;
+ }
+
+ public void SetValue(object? complexObject, IInternalEntry entry, IComplexProperty property)
+ {
+ var index = property.GetIndex();
+ Check.DebugAssert(index != -1 && index < _entries.Length, "Invalid index on complex property " + property.Name);
+ Check.DebugAssert(!IsEmpty, "Complex entries are empty");
+
+ var complexEntry = _entries[index];
+ if (complexEntry == null)
+ {
+ complexEntry = new InternalComplexEntry(entry.StateManager, property.ComplexType, entry, complexObject);
+ _entries[index] = complexEntry;
+ }
+ else
+ {
+ complexEntry.ComplexObject = complexObject;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ => _entries.Where(e => e != null).GetEnumerator()!;
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _entries.Where(e => e != null).GetEnumerator();
+
+ public bool IsEmpty
+ => _entries == null;
+ }
+}
diff --git a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs
new file mode 100644
index 00000000000..f6cc140a283
--- /dev/null
+++ b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs
@@ -0,0 +1,331 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using static Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry;
+
+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 interface IInternalEntry
+{
+ ///
+ /// 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.
+ ///
+ object? this[IPropertyBase propertyBase] { get; set; }
+
+ ///
+ /// 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.
+ ///
+ IRuntimeTypeBase StructuralType { get; }
+
+ ///
+ /// 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.
+ ///
+ bool HasConceptualNull { get; }
+
+ ///
+ /// 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.
+ ///
+ IStateManager StateManager { get; }
+
+ ///
+ /// 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.
+ ///
+ void AcceptChanges();
+
+ ///
+ /// 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.
+ ///
+ void DiscardStoreGeneratedValues();
+
+ ///
+ /// 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.
+ ///
+ object? GetCurrentValue(IPropertyBase propertyBase);
+
+ ///
+ /// 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.
+ ///
+ TProperty GetCurrentValue(IPropertyBase propertyBase);
+
+ ///
+ /// 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.
+ ///
+ object? GetOriginalValue(IPropertyBase propertyBase);
+
+ ///
+ /// 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.
+ ///
+ TProperty GetOriginalValue(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ object? GetPreStoreGeneratedCurrentValue(IPropertyBase propertyBase);
+
+ ///
+ /// 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.
+ ///
+ bool HasExplicitValue(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ bool HasTemporaryValue(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ bool IsConceptualNull(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ bool IsModified(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ bool FlaggedAsStoreGenerated(int propertyIndex);
+
+ ///
+ /// 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.
+ ///
+ bool FlaggedAsTemporary(int propertyIndex);
+
+ ///
+ /// 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.
+ ///
+ bool IsStoreGenerated(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ bool IsUnknown(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ void MarkAsTemporary(IProperty property, bool 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.
+ ///
+ void MarkUnchangedFromQuery();
+
+ ///
+ /// 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.
+ ///
+ void MarkUnknown(IProperty property);
+
+ ///
+ /// 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.
+ ///
+ IInternalEntry PrepareToSave();
+
+ ///
+ /// 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 object Object { get; } // This won't work for value types
+
+ ///
+ /// 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 void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool isCascadeDelete);
+
+ ///
+ /// 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.
+ ///
+ void PropagateValue(InternalEntityEntry principalEntry, IProperty principalProperty, IProperty dependentProperty, bool isMaterialization = false, bool setModified = true);
+
+ ///
+ /// 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.
+ ///
+ T ReadOriginalValue(IProperty property, int originalValueIndex);
+
+ ///
+ /// 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.
+ ///
+ object? ReadPropertyValue(IPropertyBase propertyBase);
+
+ ///
+ /// 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.
+ ///
+ T ReadStoreGeneratedValue(int storeGeneratedIndex);
+
+ ///
+ /// 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.
+ ///
+ T ReadTemporaryValue(int storeGeneratedIndex);
+
+ ///
+ /// 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.
+ ///
+ T ReadShadowValue(int shadowIndex);
+
+ ///
+ /// 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.
+ ///
+ void SetOriginalValue(IPropertyBase propertyBase, object? value, int index = -1);
+
+ ///
+ /// 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.
+ ///
+ void SetProperty(IPropertyBase propertyBase, object? value, bool isMaterialization, bool setModified = true, bool isCascadeDelete = 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.
+ ///
+ void SetPropertyModified(IProperty property, bool changeState = true, bool isModified = true, bool isConceptualNull = false, bool acceptChanges = 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.
+ ///
+ void SetEntityState(
+ EntityState entityState,
+ bool acceptChanges = false,
+ bool modifyProperties = true);
+
+ ///
+ /// 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.
+ ///
+ IInternalEntry GetComplexPropertyEntry(IComplexProperty property);
+
+ ///
+ /// 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.
+ ///
+ void OnComplexPropertyModified(IComplexProperty property, bool isModified = true);
+
+ ///
+ /// 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.
+ ///
+ void SetStoreGeneratedValue(IProperty property, object? value, bool setModified = true);
+
+ ///
+ /// 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.
+ ///
+ void SetTemporaryValue(IProperty property, object? value, bool setModified = true);
+}
diff --git a/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs
new file mode 100644
index 00000000000..2184b4bd373
--- /dev/null
+++ b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs
@@ -0,0 +1,1159 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+
+namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+
+public sealed partial class InternalEntityEntry
+{
+ private sealed class InternalComplexEntry : IInternalEntry
+ {
+ private readonly StateData _stateData;
+ private OriginalValues _originalValues;
+ private SidecarValues _temporaryValues;
+ private SidecarValues _storeGeneratedValues;
+ private object? complexObject;
+ private readonly ISnapshot _shadowValues;
+ private readonly ComplexEntries _complexEntries;
+
+ ///
+ /// 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 InternalComplexEntry(
+ IStateManager stateManager,
+ IComplexType complexType,
+ IInternalEntry containingEntry,
+ object? complexObject) // This works only for non-value types
+ {
+ Check.DebugAssert(complexObject == null || complexType.ClrType.IsAssignableFrom(complexObject.GetType()),
+ $"Expected {complexType.ClrType}, got {complexObject?.GetType()}");
+ StateManager = stateManager;
+ ComplexType = (IRuntimeComplexType)complexType;
+ ContainingEntry = containingEntry;
+ ComplexObject = complexObject;
+ _shadowValues = ComplexType.EmptyShadowValuesFactory();
+ _stateData = new StateData(ComplexType.PropertyCount, ComplexType.NavigationCount);
+ _complexEntries = new ComplexEntries(this);
+
+ foreach (var property in complexType.GetProperties())
+ {
+ if (property.IsShadowProperty())
+ {
+ _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Unknown, true);
+ }
+ }
+ }
+
+ ///
+ /// 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 InternalComplexEntry(
+ IStateManager stateManager,
+ IComplexType complexType,
+ IInternalEntry containingEntry,
+ object? complexObject,
+ in ValueBuffer valueBuffer)
+ {
+ Check.DebugAssert(complexObject == null || complexType.ClrType.IsAssignableFrom(complexObject.GetType()),
+ $"Expected {complexType.ClrType}, got {complexObject?.GetType()}");
+ StateManager = stateManager;
+ ComplexType = (IRuntimeComplexType)complexType;
+ ContainingEntry = containingEntry;
+ ComplexObject = complexObject;
+ _shadowValues = ComplexType.ShadowValuesFactory(valueBuffer);
+ _stateData = new StateData(ComplexType.PropertyCount, ComplexType.NavigationCount);
+ _complexEntries = new ComplexEntries(this);
+ }
+
+ ///
+ /// 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 IInternalEntry ContainingEntry { get; }
+
+ ///
+ /// 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 object? ComplexObject
+ {
+ get => complexObject;
+ set
+ {
+ Check.DebugAssert(value == null || ComplexType.ClrType.IsAssignableFrom(value.GetType()),
+ $"Expected {ComplexType.ClrType}, got {value?.GetType()}");
+ complexObject = value;
+ }
+ }
+
+ ///
+ /// 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 IRuntimeComplexType ComplexType { get; }
+
+ ///
+ /// 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 IStateManager StateManager { [DebuggerStepThrough] get; }
+
+ ///
+ /// 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 void SetEntityState(
+ EntityState entityState,
+ bool acceptChanges = false,
+ bool modifyProperties = true)
+ {
+ var oldState = _stateData.EntityState;
+ PrepareForAdd(entityState);
+
+ SetEntityState(oldState, entityState, acceptChanges, modifyProperties);
+ }
+
+ private bool PrepareForAdd(EntityState newState)
+ {
+ if (newState != EntityState.Added
+ || EntityState == EntityState.Added)
+ {
+ return false;
+ }
+
+ if (EntityState == EntityState.Modified)
+ {
+ _stateData.FlagAllProperties(
+ ComplexType.PropertyCount, PropertyFlag.Modified,
+ flagged: false);
+ }
+
+ return true;
+ }
+
+ private void SetEntityState(EntityState oldState, EntityState newState, bool acceptChanges, bool modifyProperties)
+ {
+ var complexType = ComplexType;
+
+ // Prevent temp values from becoming permanent values
+ if (oldState == EntityState.Added
+ && newState != EntityState.Added
+ && newState != EntityState.Detached)
+ {
+ // ReSharper disable once LoopCanBeConvertedToQuery
+ foreach (var property in complexType.GetProperties())
+ {
+ if (property.IsKey() && HasTemporaryValue(property))
+ {
+ throw new InvalidOperationException(
+ CoreStrings.TempValuePersists(
+ property.Name,
+ complexType.DisplayName(), newState));
+ }
+ }
+ }
+
+ // The entity state can be Modified even if some properties are not modified so always
+ // set all properties to modified if the entity state is explicitly set to Modified.
+ if (newState == EntityState.Modified
+ && modifyProperties)
+ {
+ _stateData.FlagAllProperties(ComplexType.PropertyCount, PropertyFlag.Modified, flagged: true);
+
+ // Hot path; do not use LINQ
+ foreach (var property in complexType.GetProperties())
+ {
+ if (property.GetAfterSaveBehavior() != PropertySaveBehavior.Save)
+ {
+ _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Modified, isFlagged: false);
+ }
+ }
+
+ foreach (var complexProperty in complexType.GetComplexProperties())
+ {
+ GetComplexPropertyEntry(complexProperty)
+ .SetEntityState(EntityState.Modified, acceptChanges, modifyProperties);
+ }
+ }
+
+ if (oldState == newState)
+ {
+ return;
+ }
+
+ if (newState == EntityState.Unchanged)
+ {
+ _stateData.FlagAllProperties(
+ ComplexType.PropertyCount, PropertyFlag.Modified,
+ flagged: false);
+
+ foreach (var complexProperty in complexType.GetComplexProperties())
+ {
+ GetComplexPropertyEntry(complexProperty)
+ .SetEntityState(EntityState.Unchanged, acceptChanges, modifyProperties);
+ }
+ }
+
+ if (_stateData.EntityState != oldState)
+ {
+ _stateData.EntityState = oldState;
+ }
+
+ if (newState == EntityState.Unchanged
+ && oldState == EntityState.Modified)
+ {
+ if (acceptChanges)
+ {
+ _originalValues.AcceptChanges(this);
+ }
+ else
+ {
+ _originalValues.RejectChanges(this);
+ }
+ }
+
+ _stateData.EntityState = newState;
+
+ if (newState is EntityState.Deleted or EntityState.Detached
+ && HasConceptualNull)
+ {
+ _stateData.FlagAllProperties(ComplexType.PropertyCount, PropertyFlag.Null, flagged: false);
+ }
+
+ if (oldState is EntityState.Detached or EntityState.Unchanged)
+ {
+ if (newState is EntityState.Added or EntityState.Deleted or EntityState.Modified)
+ {
+ ContainingEntry.OnComplexPropertyModified(ComplexType.ComplexProperty, isModified: true);
+ }
+ }
+ else if (newState is EntityState.Detached or EntityState.Unchanged)
+ {
+ ContainingEntry.OnComplexPropertyModified(ComplexType.ComplexProperty, isModified: 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.
+ ///
+ public void MarkUnchangedFromQuery()
+ => _stateData.EntityState = EntityState.Unchanged;
+
+ ///
+ /// 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 EntityState EntityState
+ => _stateData.EntityState;
+
+ ///
+ /// 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 bool IsModified(IProperty property)
+ {
+ var propertyIndex = property.GetIndex();
+
+ return _stateData.EntityState == EntityState.Modified
+ && _stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.Modified)
+ && !_stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.Unknown);
+ }
+
+ ///
+ /// 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 bool IsUnknown(IProperty property)
+ => _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown);
+
+ ///
+ /// 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 void SetPropertyModified(
+ IProperty property,
+ bool changeState = true,
+ bool isModified = true,
+ bool isConceptualNull = false,
+ bool acceptChanges = false)
+ {
+ var propertyIndex = property.GetIndex();
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.Unknown, false);
+
+ var currentState = _stateData.EntityState;
+
+ if (currentState is EntityState.Added or EntityState.Detached
+ || !changeState)
+ {
+ var index = property.GetOriginalValueIndex();
+ if (index != -1 && !IsConceptualNull(property))
+ {
+ SetOriginalValue(property, this[property], index);
+ }
+
+ if (currentState == EntityState.Added)
+ {
+ if (FlaggedAsTemporary(propertyIndex)
+ && !FlaggedAsStoreGenerated(propertyIndex)
+ && !HasSentinelValue(property))
+ {
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.IsTemporary, false);
+ }
+
+ return;
+ }
+ }
+
+ if (changeState
+ && !isConceptualNull
+ && isModified
+ && !StateManager.SavingChanges
+ && property.IsKey()
+ && property.GetAfterSaveBehavior() == PropertySaveBehavior.Throw)
+ {
+ throw new InvalidOperationException(CoreStrings.KeyReadOnly(property.Name, ComplexType.DisplayName()));
+ }
+
+ if (currentState == EntityState.Deleted)
+ {
+ return;
+ }
+
+ if (changeState)
+ {
+ if (!isModified
+ && currentState != EntityState.Detached
+ && property.GetOriginalValueIndex() != -1)
+ {
+ if (acceptChanges)
+ {
+ SetOriginalValue(property, GetCurrentValue(property));
+ }
+
+ SetProperty(property, GetOriginalValue(property), isMaterialization: false, setModified: false);
+ }
+
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.Modified, isModified);
+ }
+
+ if (isModified
+ && currentState is EntityState.Unchanged or EntityState.Detached)
+ {
+ if (changeState)
+ {
+ _stateData.EntityState = EntityState.Modified;
+ ContainingEntry.OnComplexPropertyModified(ComplexType.ComplexProperty, isModified);
+ }
+ }
+ else if (currentState == EntityState.Modified
+ && changeState
+ && !isModified
+ && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified))
+ {
+ _stateData.EntityState = EntityState.Unchanged;
+ ContainingEntry.OnComplexPropertyModified(ComplexType.ComplexProperty, isModified);
+ }
+ }
+
+ ///
+ /// 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 void OnComplexPropertyModified(IComplexProperty property, bool isModified = true)
+ {
+ var currentState = _stateData.EntityState;
+ if (currentState == EntityState.Deleted)
+ {
+ return;
+ }
+
+ if (isModified
+ && currentState is EntityState.Unchanged or EntityState.Detached)
+ {
+ _stateData.EntityState = EntityState.Modified;
+ }
+ else if (currentState == EntityState.Modified
+ && !isModified
+ && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified)
+ && _complexEntries.All(e => e.EntityState == EntityState.Unchanged))
+ {
+ _stateData.EntityState = EntityState.Unchanged;
+ }
+ }
+
+ ///
+ /// 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 bool HasConceptualNull
+ => _stateData.AnyPropertiesFlagged(PropertyFlag.Null);
+
+ ///
+ /// 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 bool IsConceptualNull(IProperty property)
+ => _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Null);
+
+ ///
+ /// 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 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 void PropagateValue(
+ InternalEntityEntry principalEntry,
+ IProperty principalProperty,
+ IProperty dependentProperty,
+ bool isMaterialization = false,
+ bool setModified = true)
+ {
+ var principalValue = principalEntry[principalProperty];
+ if (principalEntry.HasTemporaryValue(principalProperty))
+ {
+ SetTemporaryValue(dependentProperty, principalValue);
+ }
+ else if (principalEntry.GetValueType(principalProperty) == CurrentValueType.StoreGenerated)
+ {
+ SetStoreGeneratedValue(dependentProperty, principalValue);
+ }
+ else
+ {
+ SetProperty(dependentProperty, principalValue, isMaterialization, setModified);
+ }
+ }
+
+ private CurrentValueType GetValueType(IProperty property)
+ => _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated)
+ ? CurrentValueType.StoreGenerated
+ : _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary)
+ ? CurrentValueType.Temporary
+ : CurrentValueType.Normal;
+
+ ///
+ /// 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 void SetTemporaryValue(IProperty property, object? value, bool setModified = true)
+ {
+ if (property.GetStoreGeneratedIndex() == -1)
+ {
+ throw new InvalidOperationException(
+ CoreStrings.TempValue(property.Name, ComplexType.DisplayName()));
+ }
+
+ SetProperty(property, value, isMaterialization: false, setModified, isCascadeDelete: false, CurrentValueType.Temporary);
+ _stateData.FlagProperty(property.GetIndex(), PropertyFlag.IsTemporary, true);
+ }
+
+ ///
+ /// 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 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
+ /// 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 void SetStoreGeneratedValue(IProperty property, object? value, bool setModified = true)
+ {
+ if (property.GetStoreGeneratedIndex() == -1)
+ {
+ throw new InvalidOperationException(
+ CoreStrings.StoreGenValue(property.Name, ComplexType.DisplayName()));
+ }
+
+ SetProperty(
+ property,
+ value,
+ isMaterialization: false,
+ setModified,
+ isCascadeDelete: false,
+ CurrentValueType.StoreGenerated);
+ }
+
+ ///
+ /// 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 void MarkUnknown(IProperty property)
+ => _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Unknown, true);
+
+ ///
+ /// 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 T ReadShadowValue(int shadowIndex)
+ => _shadowValues.GetValue(shadowIndex);
+
+ ///
+ /// 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 T ReadOriginalValue(IProperty property, int originalValueIndex)
+ => _originalValues.GetValue(this, property, originalValueIndex);
+
+ ///
+ /// 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 T ReadStoreGeneratedValue(int storeGeneratedIndex)
+ => _storeGeneratedValues.GetValue(storeGeneratedIndex);
+
+ ///
+ /// 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 T ReadTemporaryValue(int storeGeneratedIndex)
+ => _temporaryValues.GetValue(storeGeneratedIndex);
+
+ ///
+ /// 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 TProperty GetCurrentValue(IPropertyBase propertyBase)
+ => ((Func)propertyBase.GetPropertyAccessors().CurrentValueGetter)(this);
+
+ ///
+ /// 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 TProperty GetOriginalValue(IProperty property)
+ => ((Func)property.GetPropertyAccessors().OriginalValueGetter!)(this);
+
+ ///
+ /// 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 object? ReadPropertyValue(IPropertyBase propertyBase)
+ => ComplexObject == null
+ ? null
+ : propertyBase.IsShadowProperty()
+ ? _shadowValues[propertyBase.GetShadowIndex()]
+ : propertyBase.GetGetter().GetClrValue(ComplexObject);
+
+ ///
+ /// 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.
+ ///
+ private void WritePropertyValue(
+ IPropertyBase propertyBase,
+ object? value,
+ bool forMaterialization)
+ {
+ if (propertyBase.IsShadowProperty())
+ {
+ _shadowValues[propertyBase.GetShadowIndex()] = value;
+ }
+ else
+ {
+ var concretePropertyBase = (IRuntimePropertyBase)propertyBase;
+
+ var setter = forMaterialization
+ ? concretePropertyBase.MaterializationSetter
+ : concretePropertyBase.GetSetter();
+
+ Check.DebugAssert(ComplexObject != null, "null object for " + ComplexType.DisplayName());
+ setter.SetClrValue(ComplexObject, value);
+ }
+ }
+
+ ///
+ /// 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 object? GetCurrentValue(IPropertyBase propertyBase)
+ => propertyBase is not IProperty property || !IsConceptualNull(property)
+ ? this[propertyBase]
+ : null;
+
+ ///
+ /// 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 object? GetPreStoreGeneratedCurrentValue(IPropertyBase propertyBase)
+ => propertyBase is not IProperty property || !IsConceptualNull(property)
+ ? ReadPropertyValue(propertyBase)
+ : null;
+
+ ///
+ /// 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 object? GetOriginalValue(IPropertyBase propertyBase)
+ => _originalValues.GetValue(this, (IProperty)propertyBase);
+
+ ///
+ /// 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 void SetOriginalValue(
+ IPropertyBase propertyBase,
+ object? value,
+ int index = -1)
+ {
+ EnsureOriginalValues();
+
+ var property = (IProperty)propertyBase;
+
+ _originalValues.SetValue(property, value, index);
+
+ // If setting the original value results in the current value being different from the
+ // original value, then mark the property as modified.
+ if ((EntityState == EntityState.Unchanged
+ || (EntityState == EntityState.Modified && !IsModified(property)))
+ && !_stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown))
+ {
+ //((StateManager as StateManager)?.ChangeDetector as ChangeDetector)?.DetectValueChange(this, property);
+ }
+ }
+
+ ///
+ /// 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 void EnsureOriginalValues()
+ {
+ if (_originalValues.IsEmpty)
+ {
+ _originalValues = new OriginalValues(this);
+ }
+ }
+
+ ///
+ /// 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 void EnsureTemporaryValues()
+ {
+ if (_temporaryValues.IsEmpty)
+ {
+ _temporaryValues = new SidecarValues(ComplexType.TemporaryValuesFactory(this));
+ }
+ }
+
+ ///
+ /// 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 void EnsureStoreGeneratedValues()
+ {
+ if (_storeGeneratedValues.IsEmpty)
+ {
+ _storeGeneratedValues = new SidecarValues(ComplexType.StoreGeneratedValuesFactory());
+ }
+ }
+
+ ///
+ /// 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 bool HasOriginalValuesSnapshot
+ => !_originalValues.IsEmpty;
+
+ ///
+ /// 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 IInternalEntry GetComplexPropertyEntry(IComplexProperty property)
+ => _complexEntries.GetEntry(this, property);
+
+ ///
+ /// 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 object? this[IPropertyBase propertyBase]
+ {
+ get
+ {
+ var storeGeneratedIndex = propertyBase.GetStoreGeneratedIndex();
+ if (storeGeneratedIndex != -1)
+ {
+ var property = (IProperty)propertyBase;
+ var propertyIndex = property.GetIndex();
+
+ if (FlaggedAsStoreGenerated(propertyIndex))
+ {
+ return _storeGeneratedValues.GetValue(storeGeneratedIndex);
+ }
+
+ if (FlaggedAsTemporary(propertyIndex)
+ && HasSentinelValue(property))
+ {
+ return _temporaryValues.GetValue(storeGeneratedIndex);
+ }
+ }
+
+ return ReadPropertyValue(propertyBase);
+ }
+
+ set => SetProperty(propertyBase, value, isMaterialization: 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.
+ ///
+ public bool FlaggedAsStoreGenerated(int propertyIndex)
+ => !_storeGeneratedValues.IsEmpty
+ && _stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.IsStoreGenerated);
+
+ ///
+ /// 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 bool FlaggedAsTemporary(int propertyIndex)
+ => !_temporaryValues.IsEmpty
+ && _stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.IsTemporary);
+
+ ///
+ /// 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 void SetProperty(
+ IPropertyBase propertyBase,
+ object? value,
+ bool isMaterialization,
+ bool setModified = true,
+ bool isCascadeDelete = false)
+ => SetProperty(propertyBase, value, isMaterialization, setModified, isCascadeDelete, CurrentValueType.Normal);
+
+ private void SetProperty(
+ IPropertyBase propertyBase,
+ object? value,
+ bool isMaterialization,
+ bool setModified,
+ bool isCascadeDelete,
+ CurrentValueType valueType)
+ {
+ var currentValue = ReadPropertyValue(propertyBase);
+
+ var asProperty = propertyBase as IProperty;
+ int propertyIndex;
+ CurrentValueType currentValueType;
+ int storeGeneratedIndex;
+ bool valuesEqual;
+
+ if (asProperty != null)
+ {
+ propertyIndex = asProperty.GetIndex();
+ valuesEqual = AreEqual(currentValue, value, asProperty);
+ currentValueType = GetValueType(asProperty);
+ storeGeneratedIndex = asProperty.GetStoreGeneratedIndex();
+ }
+ else
+ {
+ propertyIndex = -1;
+ valuesEqual = ReferenceEquals(currentValue, value);
+ currentValueType = CurrentValueType.Normal;
+ storeGeneratedIndex = -1;
+ }
+
+ if (!valuesEqual
+ || (propertyIndex != -1
+ && (_stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.Unknown)
+ || _stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.Null)
+ || valueType != currentValueType)))
+ {
+ var writeValue = true;
+
+ if (asProperty != null
+ && valueType == CurrentValueType.Normal
+ && (!asProperty.ClrType.IsNullableType()
+ || asProperty.GetContainingForeignKeys().Any(
+ fk => fk is { IsRequired: true, DeleteBehavior: DeleteBehavior.Cascade or DeleteBehavior.ClientCascade }
+ && fk.DeclaringEntityType.IsAssignableFrom(ComplexType))))
+ {
+ if (value == null)
+ {
+ HandleNullForeignKey(asProperty, setModified, isCascadeDelete);
+ writeValue = false;
+ }
+ else
+ {
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.Null, isFlagged: false);
+ }
+ }
+
+ if (writeValue)
+ {
+ //StateManager.InternalEntityEntryNotifier.PropertyChanging(this, propertyBase);
+
+ if (storeGeneratedIndex == -1)
+ {
+ WritePropertyValue(propertyBase, value, isMaterialization);
+ }
+ else
+ {
+ switch (valueType)
+ {
+ case CurrentValueType.Normal:
+ WritePropertyValue(propertyBase, value, isMaterialization);
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.IsTemporary, isFlagged: false);
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.IsStoreGenerated, isFlagged: false);
+ break;
+ case CurrentValueType.StoreGenerated:
+ EnsureStoreGeneratedValues();
+ _storeGeneratedValues.SetValue(asProperty!, value, storeGeneratedIndex);
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.IsStoreGenerated, isFlagged: true);
+ break;
+ case CurrentValueType.Temporary:
+ EnsureTemporaryValues();
+ _temporaryValues.SetValue(asProperty!, value, storeGeneratedIndex);
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.IsTemporary, isFlagged: true);
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.IsStoreGenerated, isFlagged: false);
+ if (!HasSentinelValue(asProperty!))
+ {
+ WritePropertyValue(propertyBase, value, isMaterialization);
+ }
+
+ break;
+ default:
+ Check.DebugFail($"Bad value type {valueType}");
+ break;
+ }
+ }
+
+ if (propertyIndex != -1)
+ {
+ if (_stateData.IsPropertyFlagged(propertyIndex, PropertyFlag.Unknown))
+ {
+ if (!_originalValues.IsEmpty)
+ {
+ SetOriginalValue(propertyBase, value);
+ }
+
+ _stateData.FlagProperty(propertyIndex, PropertyFlag.Unknown, isFlagged: false);
+ }
+ }
+
+ if (propertyBase is IComplexProperty complexProperty)
+ {
+ _complexEntries.SetValue(value, this, complexProperty);
+ }
+
+ //StateManager.InternalEntityEntryNotifier.PropertyChanged(this, propertyBase, setModified);
+ }
+ }
+ }
+
+ ///
+ /// 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 void HandleNullForeignKey(
+ IProperty property,
+ bool setModified = false,
+ bool isCascadeDelete = false)
+ {
+ if (EntityState != EntityState.Deleted
+ && EntityState != EntityState.Detached)
+ {
+ _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Null, isFlagged: true);
+
+ if (setModified)
+ {
+ SetPropertyModified(
+ property, changeState: true, isModified: true,
+ isConceptualNull: true);
+ }
+
+ if (!isCascadeDelete
+ && StateManager.DeleteOrphansTiming == CascadeTiming.Immediate)
+ {
+ ContainingEntry.HandleConceptualNulls(
+ StateManager.SensitiveLoggingEnabled,
+ force: false,
+ isCascadeDelete: false);
+ }
+ }
+ }
+
+ private static bool AreEqual(object? value, object? otherValue, IProperty property)
+ => property.GetValueComparer().Equals(value, otherValue);
+
+ ///
+ /// 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 void AcceptChanges()
+ {
+ if (!_storeGeneratedValues.IsEmpty)
+ {
+ foreach (var property in ComplexType.GetProperties())
+ {
+ var storeGeneratedIndex = property.GetStoreGeneratedIndex();
+ if (storeGeneratedIndex != -1
+ && _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated)
+ && _storeGeneratedValues.TryGetValue(storeGeneratedIndex, out var value))
+ {
+ this[property] = value;
+ }
+ }
+
+ _storeGeneratedValues = new SidecarValues();
+ _temporaryValues = new SidecarValues();
+ }
+
+ _stateData.FlagAllProperties(ComplexType.PropertyCount, PropertyFlag.IsStoreGenerated, false);
+ _stateData.FlagAllProperties(ComplexType.PropertyCount, PropertyFlag.IsTemporary, false);
+ _stateData.FlagAllProperties(ComplexType.PropertyCount, PropertyFlag.Unknown, false);
+
+ var currentState = EntityState;
+ switch (currentState)
+ {
+ case EntityState.Unchanged:
+ case EntityState.Detached:
+ return;
+ case EntityState.Added:
+ case EntityState.Modified:
+ _originalValues.AcceptChanges(this);
+
+ SetEntityState(EntityState.Unchanged, true);
+ break;
+ case EntityState.Deleted:
+ SetEntityState(EntityState.Detached);
+ break;
+ }
+ }
+
+ ///
+ /// 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 IInternalEntry PrepareToSave()
+ {
+ var entityType = ComplexType;
+
+ if (EntityState == EntityState.Added)
+ {
+ foreach (var property in entityType.GetProperties())
+ {
+ if (property.GetBeforeSaveBehavior() == PropertySaveBehavior.Throw
+ && !HasTemporaryValue(property)
+ && HasExplicitValue(property))
+ {
+ throw new InvalidOperationException(
+ CoreStrings.PropertyReadOnlyBeforeSave(
+ property.Name,
+ ComplexType.DisplayName()));
+ }
+
+ if (property.IsKey()
+ && property.IsForeignKey()
+ && _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown)
+ && !IsStoreGenerated(property))
+ {
+ if (property.GetContainingForeignKeys().Any(fk => fk.IsOwnership))
+ {
+ throw new InvalidOperationException(CoreStrings.SaveOwnedWithoutOwner(entityType.DisplayName()));
+ }
+
+ throw new InvalidOperationException(CoreStrings.UnknownKeyValue(entityType.DisplayName(), property.Name));
+ }
+ }
+ }
+ else if (EntityState == EntityState.Modified)
+ {
+ foreach (var property in entityType.GetProperties())
+ {
+ if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Throw
+ && IsModified(property))
+ {
+ throw new InvalidOperationException(
+ CoreStrings.PropertyReadOnlyAfterSave(
+ property.Name,
+ ComplexType.DisplayName()));
+ }
+
+ CheckForUnknownKey(property);
+ }
+ }
+ else if (EntityState == EntityState.Deleted)
+ {
+ foreach (var property in entityType.GetProperties())
+ {
+ CheckForUnknownKey(property);
+ }
+ }
+
+ DiscardStoreGeneratedValues();
+
+ return this;
+
+ void CheckForUnknownKey(IProperty property)
+ {
+ if (property.IsKey()
+ && _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.Unknown))
+ {
+ throw new InvalidOperationException(CoreStrings.UnknownShadowKeyValue(entityType.DisplayName(), property.Name));
+ }
+ }
+ }
+ ///
+ /// 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 void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool isCascadeDelete)
+ => ContainingEntry.HandleConceptualNulls(sensitiveLoggingEnabled, force, isCascadeDelete);
+
+ ///
+ /// 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 void DiscardStoreGeneratedValues()
+ {
+ if (!_storeGeneratedValues.IsEmpty)
+ {
+ _storeGeneratedValues = new SidecarValues();
+ _stateData.FlagAllProperties(ComplexType.PropertyCount, PropertyFlag.IsStoreGenerated, 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.
+ ///
+ public bool IsStoreGenerated(IProperty property)
+ => (property.ValueGenerated.ForAdd()
+ && EntityState == EntityState.Added
+ && (property.GetBeforeSaveBehavior() == PropertySaveBehavior.Ignore
+ || HasTemporaryValue(property)
+ || !HasExplicitValue(property)))
+ || (property.ValueGenerated.ForUpdate()
+ && (EntityState is EntityState.Modified or EntityState.Deleted)
+ && (property.GetAfterSaveBehavior() == PropertySaveBehavior.Ignore
+ || !IsModified(property)));
+
+ ///
+ /// 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.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool HasExplicitValue(IProperty property)
+ => !HasSentinelValue(property)
+ || _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsStoreGenerated)
+ || _stateData.IsPropertyFlagged(property.GetIndex(), PropertyFlag.IsTemporary);
+
+ private bool HasSentinelValue(IProperty property)
+ => property.IsShadowProperty()
+ ? AreEqual(_shadowValues[property.GetShadowIndex()], property.Sentinel, property)
+ : property.GetGetter().HasSentinelValue(ComplexObject!);
+
+ IRuntimeTypeBase IInternalEntry.StructuralType
+ => ComplexType;
+
+ object IInternalEntry.Object
+ => ComplexObject!;
+ }
+}
diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
index fdfa9112ba3..b0594b01178 100644
--- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
+++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
@@ -16,7 +16,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
/// 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 sealed partial class InternalEntityEntry : IUpdateEntry
+public sealed partial class InternalEntityEntry : IUpdateEntry, IInternalEntry
{
private readonly StateData _stateData;
private OriginalValues _originalValues;
@@ -24,6 +24,7 @@ public sealed partial class InternalEntityEntry : IUpdateEntry
private SidecarValues _temporaryValues;
private SidecarValues _storeGeneratedValues;
private readonly ISnapshot _shadowValues;
+ private readonly ComplexEntries _complexEntries;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -41,8 +42,15 @@ public InternalEntityEntry(
Entity = entity;
_shadowValues = EntityType.EmptyShadowValuesFactory();
_stateData = new StateData(EntityType.PropertyCount, EntityType.NavigationCount);
+ _complexEntries = new ComplexEntries(this);
- MarkShadowPropertiesNotSet(entityType);
+ foreach (var property in entityType.GetProperties())
+ {
+ if (property.IsShadowProperty())
+ {
+ _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Unknown, true);
+ }
+ }
}
///
@@ -62,6 +70,8 @@ public InternalEntityEntry(
Entity = entity;
_shadowValues = EntityType.ShadowValuesFactory(valueBuffer);
_stateData = new StateData(EntityType.PropertyCount, EntityType.NavigationCount);
+ // TODO: Set shadow properties on complex types
+ _complexEntries = new ComplexEntries(this);
}
///
@@ -313,6 +323,12 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc
_stateData.FlagProperty(property.GetIndex(), PropertyFlag.Modified, isFlagged: false);
}
}
+
+ foreach (var complexProperty in entityType.GetComplexProperties())
+ {
+ GetComplexPropertyEntry(complexProperty)
+ .SetEntityState(EntityState.Modified, acceptChanges, modifyProperties);
+ }
}
if (oldState == newState)
@@ -325,6 +341,12 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc
_stateData.FlagAllProperties(
EntityType.PropertyCount, PropertyFlag.Modified,
flagged: false);
+
+ foreach (var complexProperty in entityType.GetComplexProperties())
+ {
+ GetComplexPropertyEntry(complexProperty)
+ .SetEntityState(EntityState.Unchanged, acceptChanges, modifyProperties);
+ }
}
if (_stateData.EntityState != oldState)
@@ -414,7 +436,7 @@ private void HandleSharedIdentityEntry(EntityState newState)
throw new InvalidOperationException(
CoreStrings.IdentityConflictSensitive(
EntityType.DisplayName(),
- this.BuildCurrentValuesString(EntityType.FindPrimaryKey()!.Properties)));
+ BuildCurrentValuesString(EntityType.FindPrimaryKey()!.Properties)));
}
throw new InvalidOperationException(
@@ -493,7 +515,7 @@ private void SetServiceProperties(EntityState oldState, EntityState newState)
{
foreach (var serviceProperty in EntityType.GetServiceProperties())
{
- if (!(this[serviceProperty] is IInjectableService detachable)
+ if (this[serviceProperty] is not IInjectableService detachable
|| detachable.Detaching(Context, Entity))
{
this[serviceProperty] = null;
@@ -664,6 +686,34 @@ public void SetPropertyModified(
}
}
+ ///
+ /// 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 void OnComplexPropertyModified(IComplexProperty property, bool isModified = true)
+ {
+ var currentState = _stateData.EntityState;
+ if (currentState == EntityState.Deleted)
+ {
+ return;
+ }
+
+ if (isModified
+ && currentState is EntityState.Unchanged or EntityState.Detached)
+ {
+ _stateData.EntityState = EntityState.Modified;
+ }
+ else if (currentState == EntityState.Modified
+ && !isModified
+ && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified)
+ && _complexEntries.All(e => e.EntityState == EntityState.Unchanged))
+ {
+ _stateData.EntityState = EntityState.Unchanged;
+ }
+ }
+
///
/// 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
@@ -753,6 +803,24 @@ public void SetTemporaryValue(IProperty property, object? value, bool setModifie
public 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
+ /// 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 static readonly MethodInfo FlaggedAsTemporaryMethod
+ = typeof(IInternalEntry).GetMethod(nameof(IInternalEntry.FlaggedAsTemporary))!;
+
+ ///
+ /// 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 static readonly MethodInfo FlaggedAsStoreGeneratedMethod
+ = typeof(IInternalEntry).GetMethod(nameof(IInternalEntry.FlaggedAsStoreGenerated))!;
+
///
/// 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
@@ -776,23 +844,6 @@ public void SetStoreGeneratedValue(IProperty property, object? value, bool setMo
CurrentValueType.StoreGenerated);
}
- ///
- /// 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.
- ///
- private void MarkShadowPropertiesNotSet(IEntityType entityType)
- {
- foreach (var property in entityType.GetProperties())
- {
- if (property.IsShadowProperty())
- {
- _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Unknown, true);
- }
- }
- }
-
///
/// 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
@@ -803,7 +854,7 @@ public void MarkUnknown(IProperty property)
=> _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Unknown, true);
internal static MethodInfo MakeReadShadowValueMethod(Type type)
- => typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadShadowValue))!
+ => typeof(IInternalEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadShadowValue))!
.MakeGenericMethod(type);
///
@@ -812,11 +863,11 @@ internal static MethodInfo MakeReadShadowValueMethod(Type type)
/// 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.
///
- private T ReadShadowValue(int shadowIndex)
+ public T ReadShadowValue(int shadowIndex)
=> _shadowValues.GetValue(shadowIndex);
private static readonly MethodInfo ReadOriginalValueMethod
- = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadOriginalValue))!;
+ = typeof(IInternalEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadOriginalValue))!;
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
@@ -858,7 +909,7 @@ internal static MethodInfo MakeReadStoreGeneratedValueMethod(Type type)
=> ReadStoreGeneratedValueMethod.MakeGenericMethod(type);
private static readonly MethodInfo ReadStoreGeneratedValueMethod
- = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadStoreGeneratedValue))!;
+ = typeof(IInternalEntry).GetTypeInfo().GetDeclaredMethod(nameof(ReadStoreGeneratedValue))!;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -870,7 +921,7 @@ public T ReadStoreGeneratedValue(int storeGeneratedIndex)
=> _storeGeneratedValues.GetValue(storeGeneratedIndex);
private static readonly MethodInfo ReadTemporaryValueMethod
- = typeof(InternalEntityEntry).GetMethod(nameof(ReadTemporaryValue))!;
+ = typeof(IInternalEntry).GetMethod(nameof(ReadTemporaryValue))!;
[UnconditionalSuppressMessage(
"ReflectionAnalysis", "IL2060",
@@ -888,7 +939,7 @@ public T ReadTemporaryValue(int storeGeneratedIndex)
=> _temporaryValues.GetValue(storeGeneratedIndex);
private static readonly MethodInfo GetCurrentValueMethod
- = typeof(InternalEntityEntry).GetTypeInfo().GetDeclaredMethods(nameof(GetCurrentValue)).Single(
+ = typeof(IInternalEntry).GetTypeInfo().GetDeclaredMethods(nameof(GetCurrentValue)).Single(
m => m.IsGenericMethod);
[UnconditionalSuppressMessage(
@@ -904,7 +955,7 @@ internal static MethodInfo MakeGetCurrentValueMethod(Type type)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public TProperty GetCurrentValue(IPropertyBase propertyBase)
- => ((Func)propertyBase.GetPropertyAccessors().CurrentValueGetter)(this);
+ => ((Func)propertyBase.GetPropertyAccessors().CurrentValueGetter)(this);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -913,7 +964,7 @@ public TProperty GetCurrentValue(IPropertyBase propertyBase)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public TProperty GetOriginalValue(IProperty property)
- => ((Func)property.GetPropertyAccessors().OriginalValueGetter!)(this);
+ => ((Func)property.GetPropertyAccessors().OriginalValueGetter!)(this);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -1189,6 +1240,15 @@ public bool HasOriginalValuesSnapshot
public bool HasRelationshipSnapshot
=> !_relationshipsSnapshot.IsEmpty;
+ ///
+ /// 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 IInternalEntry GetComplexPropertyEntry(IComplexProperty property)
+ => _complexEntries.GetEntry(this, property);
+
///
/// 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
@@ -1411,6 +1471,11 @@ private void SetProperty(
SetIsLoaded(navigation, value != null);
}
+ if (propertyBase is IComplexProperty complexProperty)
+ {
+ _complexEntries.SetValue(value, this, complexProperty);
+ }
+
StateManager.InternalEntityEntryNotifier.PropertyChanged(this, propertyBase, setModified);
}
}
@@ -1823,7 +1888,7 @@ public void HandleINotifyPropertyChanging(
StateManager.InternalEntityEntryNotifier.PropertyChanging(this, propertyBase);
if (propertyBase is INavigationBase { IsCollection: true } navigation
- && GetCurrentValue(propertyBase) != null)
+ && GetCurrentValue(navigation) != null)
{
StateManager.Dependencies.InternalEntityEntrySubscriber.UnsubscribeCollectionChanged(this, navigation);
}
@@ -1845,7 +1910,7 @@ public void HandleINotifyPropertyChanged(
StateManager.InternalEntityEntryNotifier.PropertyChanged(this, propertyBase, setModified: true);
if (propertyBase is INavigationBase { IsCollection: true } navigation
- && GetCurrentValue(propertyBase) != null)
+ && GetCurrentValue(navigation) != null)
{
StateManager.Dependencies.InternalEntityEntrySubscriber.SubscribeCollectionChanged(this, navigation);
}
@@ -1982,6 +2047,24 @@ public bool IsLoaded(INavigationBase navigation)
return lazyLoaderProperty != null ? (ILazyLoader?)this[lazyLoaderProperty] : null;
}
+ ///
+ /// 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 string BuildCurrentValuesString(IEnumerable properties)
+ => ((IInternalEntry)this).BuildCurrentValuesString(properties);
+
+ ///
+ /// 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 string BuildOriginalValuesString(IEnumerable properties)
+ => ((IInternalEntry)this).BuildOriginalValuesString(properties);
+
///
/// 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
@@ -2008,6 +2091,18 @@ public DebugView DebugView
IEntityType IUpdateEntry.EntityType
=> EntityType;
+ IRuntimeTypeBase IInternalEntry.StructuralType
+ => EntityType;
+
+ object IInternalEntry.Object
+ => Entity;
+
+ IInternalEntry IInternalEntry.PrepareToSave()
+ => PrepareToSave();
+
+ void IInternalEntry.SetEntityState(EntityState entityState, bool acceptChanges, bool modifyProperties)
+ => SetEntityState(entityState, acceptChanges, modifyProperties);
+
private enum CurrentValueType
{
Normal,
diff --git a/src/EFCore/ChangeTracking/Internal/OriginalValues.cs b/src/EFCore/ChangeTracking/Internal/OriginalValues.cs
index b25b6c49bd1..3b4f2915c05 100644
--- a/src/EFCore/ChangeTracking/Internal/OriginalValues.cs
+++ b/src/EFCore/ChangeTracking/Internal/OriginalValues.cs
@@ -11,12 +11,12 @@ private readonly struct OriginalValues
{
private readonly ISnapshot _values;
- public OriginalValues(InternalEntityEntry entry)
+ public OriginalValues(IInternalEntry entry)
{
- _values = ((IRuntimeEntityType)entry.EntityType).OriginalValuesFactory(entry);
+ _values = entry.StructuralType.OriginalValuesFactory(entry);
}
- public object? GetValue(InternalEntityEntry entry, IProperty property)
+ public object? GetValue(IInternalEntry entry, IProperty property)
{
var index = property.GetOriginalValueIndex();
if (index == -1)
@@ -28,7 +28,7 @@ public OriginalValues(InternalEntityEntry entry)
return IsEmpty ? entry[property] : _values[index];
}
- public T GetValue(InternalEntityEntry entry, IProperty property, int index)
+ public T GetValue(IInternalEntry entry, IProperty property, int index)
{
if (index == -1)
{
@@ -65,14 +65,14 @@ public void SetValue(IProperty property, object? value, int index)
_values[index] = SnapshotValue(property, value);
}
- public void RejectChanges(InternalEntityEntry entry)
+ public void RejectChanges(IInternalEntry entry)
{
if (IsEmpty)
{
return;
}
- foreach (var property in entry.EntityType.GetProperties())
+ foreach (var property in entry.StructuralType.GetProperties())
{
var index = property.GetOriginalValueIndex();
if (index >= 0)
@@ -82,14 +82,14 @@ public void RejectChanges(InternalEntityEntry entry)
}
}
- public void AcceptChanges(InternalEntityEntry entry)
+ public void AcceptChanges(IInternalEntry entry)
{
if (IsEmpty)
{
return;
}
- foreach (var property in entry.EntityType.GetProperties())
+ foreach (var property in entry.StructuralType.GetProperties())
{
var index = property.GetOriginalValueIndex();
if (index >= 0)
diff --git a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs
index d2fa8816177..f0a15b610de 100644
--- a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs
@@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
/// 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 OriginalValuesFactoryFactory : SnapshotFactoryFactory
+public class OriginalValuesFactoryFactory : SnapshotFactoryFactory
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs
index 40fcd76ae1b..d43f8eba4b1 100644
--- a/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs
@@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
/// 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 SidecarValuesFactoryFactory : SnapshotFactoryFactory
+public class SidecarValuesFactoryFactory : SnapshotFactoryFactory
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs
index 7ac738c4966..3f4334aa65b 100644
--- a/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SimpleFullyNullableDependentKeyValueFactory.cs
@@ -61,7 +61,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen
///
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = ((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry);
+ key = ((Func)_propertyAccessors.CurrentValueGetter)((IInternalEntry)entry);
return key != null;
}
@@ -73,7 +73,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen
///
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry);
+ key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((IInternalEntry)entry);
return key != null;
}
@@ -85,7 +85,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
///
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = ((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry);
+ key = ((Func)_propertyAccessors.OriginalValueGetter!)((IInternalEntry)entry);
return key != null;
}
diff --git a/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs
index 2c9162b6ab5..c41962e0e35 100644
--- a/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SimpleNonNullableDependentKeyValueFactory.cs
@@ -68,7 +68,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen
///
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = ((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry)!;
+ key = ((Func)_propertyAccessors.CurrentValueGetter)((IInternalEntry)entry)!;
return true;
}
@@ -80,7 +80,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen
///
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry)!;
+ key = ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((IInternalEntry)entry)!;
return true;
}
@@ -92,7 +92,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
///
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = ((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry)!;
+ key = ((Func)_propertyAccessors.OriginalValueGetter!)((IInternalEntry)entry)!;
return true;
}
diff --git a/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs
index ce52fb1cd8d..85b357c0e13 100644
--- a/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SimpleNullableDependentKeyValueFactory.cs
@@ -66,7 +66,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, out TKey key
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key)
- => HandleNullableValue(((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry), out key);
+ => HandleNullableValue(((Func)_propertyAccessors.CurrentValueGetter)((IInternalEntry)entry), out key);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -76,7 +76,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, out TKey key
///
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, out TKey key)
=> HandleNullableValue(
- ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry), out key);
+ ((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((IInternalEntry)entry), out key);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -85,7 +85,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, out TKey key)
- => HandleNullableValue(((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry), out key);
+ => HandleNullableValue(((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry), out key);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs b/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs
index b350bec9434..a32ed396555 100644
--- a/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SimpleNullablePrincipalDependentKeyValueFactory.cs
@@ -72,7 +72,7 @@ public virtual bool TryCreateFromBuffer(in ValueBuffer valueBuffer, [NotNullWhen
///
public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = (TKey)(object)((Func)_propertyAccessors.CurrentValueGetter)((InternalEntityEntry)entry)!;
+ key = (TKey)(object)((Func)_propertyAccessors.CurrentValueGetter)((IInternalEntry)entry)!;
return true;
}
@@ -84,7 +84,7 @@ public override bool TryCreateFromCurrentValues(IUpdateEntry entry, [NotNullWhen
///
public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = (TKey)(object)((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((InternalEntityEntry)entry)!;
+ key = (TKey)(object)((Func)_propertyAccessors.PreStoreGeneratedCurrentValueGetter)((IInternalEntry)entry)!;
return true;
}
@@ -96,7 +96,7 @@ public virtual bool TryCreateFromPreStoreGeneratedCurrentValues(IUpdateEntry ent
///
public override bool TryCreateFromOriginalValues(IUpdateEntry entry, [NotNullWhen(true)] out TKey? key)
{
- key = (TKey)(object)((Func)_propertyAccessors.OriginalValueGetter!)((InternalEntityEntry)entry)!;
+ key = (TKey)(object)((Func)_propertyAccessors.OriginalValueGetter!)((IInternalEntry)entry)!;
return true;
}
diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
index c69e316928a..49a7381720f 100644
--- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs
@@ -165,7 +165,9 @@ protected virtual Expression CreateSnapshotExpression(
Expression.Assign(
entityVariable,
Expression.Convert(
- Expression.Property(parameter!, "Entity"),
+ Expression.Property(parameter!, parameter!.Type == typeof(InternalEntityEntry)
+ ? nameof(InternalEntityEntry.Entity)
+ : nameof(IInternalEntry.Object)),
entityType!)),
constructorExpression
})
diff --git a/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs
index 0ecc939920d..e422ae610c7 100644
--- a/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs
+++ b/src/EFCore/ChangeTracking/Internal/TemporaryValuesFactoryFactory.cs
@@ -28,7 +28,7 @@ protected override Expression CreateSnapshotExpression(
var constructorExpression = Expression.Convert(
Expression.New(
Snapshot.CreateSnapshotType(types).GetDeclaredConstructor(types)!,
- types.Select(e => Expression.Default(e)).ToArray()),
+ types.Select(Expression.Default).ToArray()),
typeof(ISnapshot));
return constructorExpression;
diff --git a/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs b/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs
index 1686a1f695f..0c45b3779ec 100644
--- a/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs
+++ b/src/EFCore/ChangeTracking/Internal/ValueGenerationManager.cs
@@ -93,6 +93,7 @@ public virtual bool Generate(InternalEntityEntry entry, bool includePrimaryKey =
var hasStableValues = false;
var hasNonStableValues = false;
+ //TODO: Handle complex properties
foreach (var property in entry.EntityType.GetValueGeneratingProperties())
{
if (entry.HasExplicitValue(property)
diff --git a/src/EFCore/Metadata/Internal/ComplexType.cs b/src/EFCore/Metadata/Internal/ComplexType.cs
index b72ccf3a1b0..6c22afea45b 100644
--- a/src/EFCore/Metadata/Internal/ComplexType.cs
+++ b/src/EFCore/Metadata/Internal/ComplexType.cs
@@ -28,8 +28,8 @@ public class ComplexType : TypeBase, IMutableComplexType, IConventionComplexType
private InstantiationBinding? _constructorBinding;
private InstantiationBinding? _serviceOnlyConstructorBinding;
- private Func? _originalValuesFactory;
- private Func? _temporaryValuesFactory;
+ private Func? _originalValuesFactory;
+ private Func? _temporaryValuesFactory;
private Func? _storeGeneratedValuesFactory;
private Func? _shadowValuesFactory;
private Func? _emptyShadowValuesFactory;
@@ -382,7 +382,7 @@ public virtual PropertyCounts Counts
/// 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 OriginalValuesFactory
+ public virtual Func OriginalValuesFactory
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _originalValuesFactory, this,
static complexType =>
@@ -412,7 +412,7 @@ public virtual Func StoreGeneratedValuesFactory
/// 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 TemporaryValuesFactory
+ public virtual Func TemporaryValuesFactory
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _temporaryValuesFactory, this,
static complexType =>
diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs
index fd8cdc105b2..40603623ca8 100644
--- a/src/EFCore/Metadata/Internal/EntityType.cs
+++ b/src/EFCore/Metadata/Internal/EntityType.cs
@@ -61,8 +61,8 @@ private readonly SortedDictionary _triggers
private InstantiationBinding? _serviceOnlyConstructorBinding;
private Func? _relationshipSnapshotFactory;
- private Func? _originalValuesFactory;
- private Func? _temporaryValuesFactory;
+ private Func? _originalValuesFactory;
+ private Func? _temporaryValuesFactory;
private Func? _storeGeneratedValuesFactory;
private Func? _shadowValuesFactory;
private Func? _emptyShadowValuesFactory;
@@ -2277,7 +2277,7 @@ public virtual Func RelationshipSnapshotFactory
/// 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 OriginalValuesFactory
+ public virtual Func OriginalValuesFactory
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _originalValuesFactory, this,
static entityType =>
@@ -2307,7 +2307,7 @@ public virtual Func StoreGeneratedValuesFactory
/// 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 TemporaryValuesFactory
+ public virtual Func TemporaryValuesFactory
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _temporaryValuesFactory, this,
static entityType =>
diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs
index bbe05b057be..841abb56138 100644
--- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs
+++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs
@@ -172,6 +172,7 @@ public static PropertyCounts CalculateCounts(this IRuntimeEntityType entityType)
{
propertyIndex = baseCounts.PropertyCount;
navigationIndex = baseCounts.NavigationCount;
+ complexPropertyIndex = baseCounts.ComplexPropertyCount;
originalValueIndex = baseCounts.OriginalValueCount;
shadowIndex = baseCounts.ShadowCount;
relationshipIndex = baseCounts.RelationshipCount;
diff --git a/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs b/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs
index 6bfa4bbd60f..43fcd3992c6 100644
--- a/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs
+++ b/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs
@@ -19,7 +19,7 @@ public interface IRuntimeTypeBase : ITypeBase
/// 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 OriginalValuesFactory { get; }
+ Func OriginalValuesFactory { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -35,7 +35,7 @@ public interface IRuntimeTypeBase : ITypeBase
/// 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 TemporaryValuesFactory { get; }
+ Func TemporaryValuesFactory { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -115,6 +115,15 @@ int RelationshipPropertyCount
int NavigationCount
=> Counts.NavigationCount;
+ ///
+ /// 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.
+ ///
+ int ComplexPropertyCount
+ => Counts.ComplexPropertyCount;
+
///
/// 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
diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
index 512d781c674..4152e5ed1ff 100644
--- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
+++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs
@@ -42,12 +42,12 @@ private static PropertyAccessors CreateGeneric(IPropertyBase property
property == null ? null : CreateValueBufferGetter(property));
}
- private static Func CreateCurrentValueGetter(
+ private static Func CreateCurrentValueGetter(
IPropertyBase propertyBase,
bool useStoreGeneratedValues)
{
var entityClrType = propertyBase.DeclaringType.ClrType;
- var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry");
+ var entryParameter = Expression.Parameter(typeof(IInternalEntry), "entry");
var propertyIndex = propertyBase.GetIndex();
var shadowIndex = propertyBase.GetShadowIndex();
var storeGeneratedIndex = propertyBase.GetStoreGeneratedIndex();
@@ -66,7 +66,7 @@ private static Func CreateCurrentValueGetter CreateCurrentValueGetter CreateCurrentValueGetter CreateCurrentValueGetter CreateCurrentValueGetter>(
+ return Expression.Lambda>(
currentValueExpression,
entryParameter)
.Compile();
}
- private static Func CreateOriginalValueGetter(IProperty property)
+ private static Func CreateOriginalValueGetter(IProperty property)
{
- var entryParameter = Expression.Parameter(typeof(InternalEntityEntry), "entry");
+ var entryParameter = Expression.Parameter(typeof(IInternalEntry), "entry");
var originalValuesIndex = property.GetOriginalValueIndex();
- return Expression.Lambda>(
+ return Expression.Lambda>(
originalValuesIndex >= 0
? Expression.Call(
entryParameter,
diff --git a/src/EFCore/Metadata/RuntimeTypeBase.cs b/src/EFCore/Metadata/RuntimeTypeBase.cs
index cf020c65607..be4e522b8af 100644
--- a/src/EFCore/Metadata/RuntimeTypeBase.cs
+++ b/src/EFCore/Metadata/RuntimeTypeBase.cs
@@ -31,8 +31,8 @@ public abstract class RuntimeTypeBase : AnnotatableBase, IRuntimeTypeBase
private readonly ChangeTrackingStrategy _changeTrackingStrategy;
// Warning: Never access these fields directly as access needs to be thread-safe
- private Func? _originalValuesFactory;
- private Func? _temporaryValuesFactory;
+ private Func? _originalValuesFactory;
+ private Func? _temporaryValuesFactory;
private Func? _storeGeneratedValuesFactory;
private Func? _shadowValuesFactory;
private Func? _emptyShadowValuesFactory;
@@ -491,7 +491,7 @@ private IEnumerable FindDerivedComplexProperties(string
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public virtual void SetOriginalValuesFactory(Func factory)
+ public virtual void SetOriginalValuesFactory(Func factory)
{
_originalValuesFactory = factory;
}
@@ -515,7 +515,7 @@ public virtual void SetStoreGeneratedValuesFactory(Func factory)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
[EntityFrameworkInternal]
- public virtual void SetTemporaryValuesFactory(Func factory)
+ public virtual void SetTemporaryValuesFactory(Func factory)
{
_temporaryValuesFactory = factory;
}
@@ -688,7 +688,7 @@ PropertyCounts IRuntimeTypeBase.Counts
}
///
- Func IRuntimeTypeBase.OriginalValuesFactory
+ Func IRuntimeTypeBase.OriginalValuesFactory
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _originalValuesFactory, this,
static complexType => RuntimeFeature.IsDynamicCodeSupported
@@ -704,7 +704,7 @@ Func IRuntimeTypeBase.StoreGeneratedValuesFactory
: throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel));
///
- Func IRuntimeTypeBase.TemporaryValuesFactory
+ Func IRuntimeTypeBase.TemporaryValuesFactory
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _temporaryValuesFactory, this,
static complexType => RuntimeFeature.IsDynamicCodeSupported
diff --git a/src/EFCore/Update/UpdateEntryExtensions.cs b/src/EFCore/Update/UpdateEntryExtensions.cs
index c6dd69bc28e..33efe07bb6e 100644
--- a/src/EFCore/Update/UpdateEntryExtensions.cs
+++ b/src/EFCore/Update/UpdateEntryExtensions.cs
@@ -25,6 +25,25 @@ public static class UpdateEntryExtensions
/// The property to get the value for.
/// The value for the property.
public static object? GetCurrentProviderValue(this IUpdateEntry updateEntry, IProperty property)
+ => GetCurrentProviderValue((IInternalEntry)updateEntry, property);
+
+ ///
+ /// Gets the original value that was assigned to the property and converts it to the provider-expected value.
+ ///
+ /// The entry.
+ /// The property to get the value for.
+ /// The value for the property.
+ public static object? GetOriginalProviderValue(this IUpdateEntry updateEntry, IProperty property)
+ => GetOriginalProviderValue((IInternalEntry)updateEntry, property);
+
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public static object? GetCurrentProviderValue(this IInternalEntry updateEntry, IProperty property)
{
var value = updateEntry.GetCurrentValue(property);
var typeMapping = property.GetTypeMapping();
@@ -42,12 +61,13 @@ public static class UpdateEntryExtensions
}
///
- /// Gets the original value that was assigned to the property and converts it to the provider-expected value.
+ /// 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.
///
- /// The entry.
- /// The property to get the value for.
- /// The value for the property.
- public static object? GetOriginalProviderValue(this IUpdateEntry updateEntry, IProperty property)
+ [EntityFrameworkInternal]
+ public static object? GetOriginalProviderValue(this IInternalEntry updateEntry, IProperty property)
{
var value = updateEntry.GetOriginalValue(property);
var typeMapping = property.GetTypeMapping();
@@ -284,6 +304,31 @@ void AppendRelatedKey(IEntityType targetType, object value)
public static string BuildCurrentValuesString(
this IUpdateEntry entry,
IEnumerable properties)
+ => BuildCurrentValuesString((IInternalEntry)entry, properties);
+
+ ///
+ /// Creates a formatted string representation of the given properties and their original
+ /// values such as is useful when throwing exceptions about keys, indexes, etc. that use
+ /// the properties.
+ ///
+ /// The entry from which values will be obtained.
+ /// The properties to format.
+ /// The string representation.
+ public static string BuildOriginalValuesString(
+ this IUpdateEntry entry,
+ IEnumerable properties)
+ => BuildOriginalValuesString((IInternalEntry)entry, properties);
+
+ ///
+ /// 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.
+ ///
+ [EntityFrameworkInternal]
+ public static string BuildCurrentValuesString(
+ this IInternalEntry entry,
+ IEnumerable properties)
=> "{"
+ string.Join(
", ", properties.Select(
@@ -299,15 +344,14 @@ public static string BuildCurrentValuesString(
+ "}";
///
- /// Creates a formatted string representation of the given properties and their original
- /// values such as is useful when throwing exceptions about keys, indexes, etc. that use
- /// the properties.
+ /// 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.
///
- /// The entry from which values will be obtained.
- /// The properties to format.
- /// The string representation.
+ [EntityFrameworkInternal]
public static string BuildOriginalValuesString(
- this IUpdateEntry entry,
+ this IInternalEntry entry,
IEnumerable properties)
=> "{"
+ string.Join(
diff --git a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs
index 553890e9114..9e5ffb9ee73 100644
--- a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs
+++ b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs
@@ -14,32 +14,6 @@ protected TableSplittingTestBase(ITestOutputHelper testOutputHelper)
// TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
}
- [ConditionalFact]
- public virtual async Task Can_update_just_dependents()
- {
- await InitializeAsync(OnModelCreating);
-
- Operator firstOperator;
- Engine firstEngine;
- using (var context = CreateContext())
- {
- firstOperator = context.Set().OrderBy(o => o.VehicleName).First();
- firstOperator.Name += "1";
- firstEngine = context.Set().OrderBy(o => o.VehicleName).First();
- firstEngine.Description += "1";
-
- context.SaveChanges();
-
- Assert.Empty(context.ChangeTracker.Entries().Where(e => e.State != EntityState.Unchanged));
- }
-
- using (var context = CreateContext())
- {
- Assert.Equal(firstOperator.Name, context.Set().OrderBy(o => o.VehicleName).First().Name);
- Assert.Equal(firstEngine.Description, context.Set().OrderBy(o => o.VehicleName).First().Description);
- }
- }
-
[ConditionalFact]
public virtual async Task Can_query_shared()
{
@@ -189,6 +163,52 @@ await InitializeAsync(
}
}
+ [ConditionalFact]
+ public virtual async Task Can_share_required_columns_with_complex_types()
+ {
+ await InitializeAsync(
+ modelBuilder =>
+ {
+ OnModelCreatingComplex(modelBuilder);
+ modelBuilder.Entity(
+ vb =>
+ {
+ vb.Property(v => v.SeatingCapacity).HasColumnName("SeatingCapacity");
+ });
+ modelBuilder.Entity(
+ vb =>
+ {
+ vb.ComplexProperty(v => v.Engine, eb =>
+ {
+ eb.Property("SeatingCapacity").HasColumnName("SeatingCapacity");
+ });
+ });
+ }, seed: false);
+
+ using (var context = CreateContext())
+ {
+ var scooterEntry = await context.AddAsync(
+ new PoweredVehicle
+ {
+ Name = "Electric scooter",
+ SeatingCapacity = 1,
+ Engine = new Engine(),
+ Operator = new Operator { Name = "Kai Saunders", Details = new OperatorDetails() }
+ });
+
+ context.SaveChanges();
+
+ //Assert.Equal(scooter.SeatingCapacity, scooterEntry.ComplexProperty(v => v.Engine).TargetEntry.Property("SeatingCapacity").CurrentValue);
+ }
+
+ //using (var context = CreateContext())
+ //{
+ // var scooter = context.Set().Single(v => v.Name == "Electric scooter");
+
+ // Assert.Equal(scooter.SeatingCapacity, context.Entry(scooter).ComplexProperty(v => v.Engine).TargetEntry.Property("SeatingCapacity").CurrentValue);
+ //}
+ }
+
[ConditionalFact]
public virtual async Task Can_use_optional_dependents_with_shared_concurrency_tokens()
{
@@ -260,6 +280,78 @@ await InitializeAsync(
}
}
+ [ConditionalFact]
+ public virtual async Task Can_use_optional_dependents_with_shared_concurrency_tokens_with_complex_types()
+ {
+ await InitializeAsync(
+ modelBuilder =>
+ {
+ OnModelCreatingComplex(modelBuilder);
+ modelBuilder.Entity(
+ vb =>
+ {
+ vb.Property(v => v.SeatingCapacity).HasColumnName("SeatingCapacity").IsConcurrencyToken();
+ });
+ modelBuilder.Entity(
+ vb =>
+ {
+ vb.ComplexProperty(v => v.Engine, eb =>
+ {
+ eb.Property("SeatingCapacity").HasColumnName("SeatingCapacity").IsConcurrencyToken();
+ });
+ });
+ }, seed: false);
+
+ using (var context = CreateContext())
+ {
+ var scooterEntry = await context.AddAsync(
+ new PoweredVehicle
+ {
+ Name = "Electric scooter",
+ SeatingCapacity = 1,
+ Operator = new Operator { Name = "Kai Saunders" }
+ });
+
+ context.SaveChanges();
+ }
+
+ using (var context = CreateContext())
+ {
+ var scooter = context.Set().Single(v => v.Name == "Electric scooter");
+
+ Assert.Equal(1, scooter.SeatingCapacity);
+
+ scooter.Engine = new Engine();
+
+ //var engineCapacityEntry = context.Entry(scooter).ComplexProperty(v => v.Engine).TargetEntry.Property("SeatingCapacity");
+
+ //Assert.Equal(0, engineCapacityEntry.OriginalValue);
+
+ context.SaveChanges();
+
+ //Assert.Equal(0, engineCapacityEntry.OriginalValue);
+ //Assert.Equal(0, engineCapacityEntry.CurrentValue);
+ }
+
+ using (var context = CreateContext())
+ {
+ var scooter = context.Set().Single(v => v.Name == "Electric scooter");
+
+ //Assert.Equal(scooter.SeatingCapacity, context.Entry(scooter).ComplexProperty(v => v.Engine).TargetEntry.Property("SeatingCapacity").CurrentValue);
+
+ scooter.SeatingCapacity = 2;
+ context.SaveChanges();
+ }
+
+ using (var context = CreateContext())
+ {
+ var scooter = context.Set().Include(v => v.Engine).Single(v => v.Name == "Electric scooter");
+
+ Assert.Equal(2, scooter.SeatingCapacity);
+ //Assert.Equal(2, context.Entry(scooter).ComplexProperty(v => v.Engine).TargetEntry.Property("SeatingCapacity").CurrentValue);
+ }
+ }
+
protected async Task Test_roundtrip(Action onModelCreating)
{
await InitializeAsync(onModelCreating);
@@ -351,28 +443,71 @@ await InitializeAsync(
}
}
- [ConditionalFact(Skip = "Issue #24970")]
+ [ConditionalFact]
+ public virtual async Task Can_update_just_dependents()
+ {
+ await InitializeAsync(OnModelCreating);
+
+ Operator firstOperator;
+ Engine firstEngine;
+ using (var context = CreateContext())
+ {
+ firstOperator = context.Set().OrderBy(o => o.VehicleName).First();
+ firstOperator.Name += "1";
+ firstEngine = context.Set().OrderBy(o => o.VehicleName).First();
+ firstEngine.Description += "1";
+
+ context.SaveChanges();
+
+ Assert.Empty(context.ChangeTracker.Entries().Where(e => e.State != EntityState.Unchanged));
+ }
+
+ using (var context = CreateContext())
+ {
+ Assert.Equal(firstOperator.Name, context.Set().OrderBy(o => o.VehicleName).First().Name);
+ Assert.Equal(firstEngine.Description, context.Set().OrderBy(o => o.VehicleName).First().Description);
+ }
+ }
+
+ [ConditionalFact]
public virtual async Task Can_insert_dependent_with_just_one_parent()
{
await InitializeAsync(OnModelCreating);
- using var context = CreateContext();
- await context.AddAsync(
- new PoweredVehicle
- {
- Name = "Fuel transport",
- SeatingCapacity = 1,
- Operator = new LicensedOperator { Name = "Jack Jackson", LicenseType = "Class A CDC" }
- });
- await context.AddAsync(
- new FuelTank
- {
- Capacity = 10000_1,
- FuelType = "Gas",
- VehicleName = "Fuel transport"
- });
+ using (var context = CreateContext())
+ {
+ await context.AddAsync(
+ new PoweredVehicle
+ {
+ Name = "Fuel transport",
+ SeatingCapacity = 1,
+ Operator = new LicensedOperator { Name = "Jack Jackson", LicenseType = "Class A CDC" }
+ });
+ await context.AddAsync(
+ new FuelTank
+ {
+ Capacity = 10000_1,
+ FuelType = "Gas",
+ VehicleName = "Fuel transport"
+ });
- context.SaveChanges();
+ context.SaveChanges();
+
+ var savedEntries = context.ChangeTracker.Entries().ToList();
+ Assert.Equal(3, savedEntries.Count);
+ Assert.All(savedEntries, e => Assert.Equal(EntityState.Unchanged, e.State));
+ }
+
+ using (var context = CreateContext())
+ {
+ var transport = context.Vehicles.Include(v => v.Operator)
+ .Single(v => v.Name == "Fuel transport");
+ var tank = context.Set().Include(v => v.Vehicle)
+ .Single(v => v.VehicleName == "Fuel transport");
+ Assert.NotNull(transport.Operator.Name);
+ Assert.Null(tank.Engine);
+ Assert.Same(transport, tank.Vehicle);
+ }
}
[ConditionalFact]
@@ -798,6 +933,43 @@ protected virtual void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.Entity().ToTable("Vehicles");
}
+ protected virtual void OnModelCreatingComplex(ModelBuilder modelBuilder)
+ {
+ OnModelCreating(modelBuilder);
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Ignore();
+ modelBuilder.Entity(
+ vb =>
+ {
+ vb.Property(v => v.Name).HasColumnName("Name");
+ vb.Ignore(v => v.Operator);
+ vb.ComplexProperty(v => v.Operator, ob =>
+ {
+ ob.IsRequired();
+ ob.Property(o => o.VehicleName).HasColumnName("Name");
+ ob.ComplexProperty(o => o.Details)
+ .IsRequired()
+ .Property(o => o.VehicleName).HasColumnName("Name");
+ });
+ });
+ modelBuilder.Entity(
+ vb =>
+ {
+ vb.Ignore(v => v.Engine);
+ vb.ComplexProperty(v => v.Engine, eb =>
+ {
+ eb.IsRequired();
+ eb.Property(o => o.VehicleName).HasColumnName("Name");
+ });
+ });
+ }
+
protected virtual void OnSharedModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity(
diff --git a/test/EFCore.Specification.Tests/F1FixtureBase.cs b/test/EFCore.Specification.Tests/F1FixtureBase.cs
index 95fa9edb64a..3edaa571645 100644
--- a/test/EFCore.Specification.Tests/F1FixtureBase.cs
+++ b/test/EFCore.Specification.Tests/F1FixtureBase.cs
@@ -166,7 +166,14 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder)
modelBuilder.Entity(
b =>
{
- b.OwnsOne(s => s.Details);
+ b.ComplexProperty(
+ s => s.Details, eb =>
+ {
+ eb.IsRequired();
+ eb.Property(d => d.Space);
+ eb.Property("Version").IsRowVersion();
+ eb.Property(Sponsor.ClientTokenPropertyName).IsConcurrencyToken();
+ });
ConfigureConstructorBinding(b.Metadata);
});
@@ -184,15 +191,6 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder)
eb.Property(Sponsor.ClientTokenPropertyName);
});
- modelBuilder.Entity()
- .OwnsOne(
- s => s.Details, eb =>
- {
- eb.Property(d => d.Space);
- eb.Property("Version").IsRowVersion();
- eb.Property(Sponsor.ClientTokenPropertyName).IsConcurrencyToken();
- });
-
modelBuilder.Entity();
modelBuilder.Entity();
modelBuilder.Entity();
diff --git a/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs
index abea81ef11a..f09fe59dcfb 100644
--- a/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs
@@ -246,4 +246,10 @@ WHEN [t0].[Active] IS NOT NULL THEN [t0].[Name]
ORDER BY [v].[Name]
""");
}
+
+ public override Task Can_insert_dependent_with_just_one_parent()
+ {
+ // This scenario is not valid for TPT
+ return Task.CompletedTask;
+ }
}