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; + } }