diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 1dc52c82723..8250dac0e8e 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -315,13 +315,13 @@ protected virtual void GenerateSequence( if (sequence.Type != Sequence.DefaultClrType) { sequenceBuilderNameBuilder - .Append("<") + .Append('<') .Append(Code.Reference(sequence.Type)) - .Append(">"); + .Append('>'); } sequenceBuilderNameBuilder - .Append("(") + .Append('(') .Append(Code.Literal(sequence.Name)); if (!string.IsNullOrEmpty(sequence.ModelSchema)) @@ -331,7 +331,7 @@ protected virtual void GenerateSequence( .Append(Code.Literal(sequence.ModelSchema)); } - sequenceBuilderNameBuilder.Append(")"); + sequenceBuilderNameBuilder.Append(')'); var sequenceBuilderName = sequenceBuilderNameBuilder.ToString(); stringBuilder @@ -347,7 +347,7 @@ protected virtual void GenerateSequence( .AppendLine() .Append(".StartsAt(") .Append(Code.Literal(sequence.StartValue)) - .Append(")"); + .Append(')'); } if (sequence.IncrementBy != Sequence.DefaultIncrementBy) @@ -356,7 +356,7 @@ protected virtual void GenerateSequence( .AppendLine() .Append(".IncrementsBy(") .Append(Code.Literal(sequence.IncrementBy)) - .Append(")"); + .Append(')'); } if (sequence.MinValue != Sequence.DefaultMinValue) @@ -374,7 +374,7 @@ protected virtual void GenerateSequence( .AppendLine() .Append(".HasMax(") .Append(Code.Literal(sequence.MaxValue)) - .Append(")"); + .Append(')'); } if (sequence.IsCyclic != Sequence.DefaultIsCyclic) @@ -586,7 +586,7 @@ protected virtual void GenerateComplexProperty( private static string GenerateNestedBuilderName(string builderName) { - if (builderName.StartsWith("b", StringComparison.Ordinal)) + if (builderName.StartsWith('b')) { // ReSharper disable once InlineOutVariableDeclaration var counter = 1; @@ -876,7 +876,7 @@ protected virtual void GenerateEntityTypeAnnotations( stringBuilder .AppendLine() .Append(entityTypeBuilderName) - .Append(".") + .Append('.') .Append("HasDiscriminator"); if (discriminatorPropertyAnnotation?.Value != null) @@ -886,11 +886,11 @@ protected virtual void GenerateEntityTypeAnnotations( .MakeNullable(discriminatorProperty.IsNullable) ?? discriminatorProperty.ClrType; stringBuilder - .Append("<") + .Append('<') .Append(Code.Reference(propertyClrType)) .Append(">(") .Append(Code.Literal((string)discriminatorPropertyAnnotation.Value)) - .Append(")"); + .Append(')'); } else { @@ -903,11 +903,11 @@ protected virtual void GenerateEntityTypeAnnotations( var value = (bool)discriminatorMappingCompleteAnnotation.Value; stringBuilder - .Append(".") + .Append('.') .Append("IsComplete") - .Append("(") + .Append('(') .Append(Code.Literal(value)) - .Append(")"); + .Append(')'); } if (discriminatorValueAnnotation?.Value != null) @@ -924,11 +924,11 @@ protected virtual void GenerateEntityTypeAnnotations( } stringBuilder - .Append(".") + .Append('.') .Append("HasValue") - .Append("(") + .Append('(') .Append(Code.UnknownLiteral(value)) - .Append(")"); + .Append(')'); } stringBuilder.AppendLine(";"); diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 36a1bbc2f65..4cab4805dea 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -4,7 +4,6 @@ using System.Text; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Internal; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal; @@ -652,7 +651,7 @@ private void Create(IEntityType entityType, CSharpRuntimeAnnotationCodeGenerator mainBuilder.AppendLine(",") .Append("indexerPropertyInfo: RuntimeEntityType.FindIndexerProperty(") .Append(_code.Literal(entityType.ClrType)) - .Append(")"); + .Append(')'); } if (entityType.IsPropertyBag) @@ -991,7 +990,7 @@ private void PropertyBaseParameters( .Append(".GetProperty(") .Append(_code.Literal(propertyInfo.Name)) .Append(", ") - .Append(propertyInfo.GetAccessors().Any() ? "BindingFlags.Public" : "BindingFlags.NonPublic") + .Append(propertyInfo.GetAccessors().Length != 0 ? "BindingFlags.Public" : "BindingFlags.NonPublic") .Append(propertyInfo.IsStatic() ? " | BindingFlags.Static" : " | BindingFlags.Instance") .Append(" | BindingFlags.DeclaredOnly)"); } @@ -1055,12 +1054,12 @@ private void FindProperties( .Append(entityTypeVariable) .Append(".FindProperty(") .Append(_code.Literal(property.Name)) - .Append(")"); + .Append(')'); if (nullable) { mainBuilder - .Append("!"); + .Append('!'); } } } diff --git a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs index 197a53c31b6..d5ab67321f6 100644 --- a/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/SharedTableConvention.cs @@ -216,7 +216,7 @@ private static void TryUniquifyColumnNames( in StoreObjectIdentifier storeObject, int maxLength) { - foreach (var property in type.GetDeclaredProperties()) + foreach (var property in type.GetProperties()) { var columnName = property.GetColumnName(storeObject); if (columnName == null) @@ -224,7 +224,8 @@ private static void TryUniquifyColumnNames( continue; } - if (!columns.TryGetValue(columnName, out var otherProperty)) + if (!columns.TryGetValue(columnName, out var otherProperty) + || property == otherProperty) { columns[columnName] = property; continue; @@ -237,13 +238,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/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs index 9dd98b18a63..1322127f8a4 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs @@ -1,8 +1,6 @@ // 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; - namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; /// @@ -79,6 +77,8 @@ public virtual void ProcessModelFinalizing( Dictionary? entityTypesMissingConcurrencyColumn = null; foreach (var entityType in mappedTypes) { + Check.DebugAssert(readOnlyProperties.Count != 0, $"No properties mapped to column '{concurrencyColumnName}'"); + var foundMappedProperty = !IsConcurrencyTokenMissing(readOnlyProperties, entityType, mappedTypes) || entityType.GetProperties() .Any(p => p.GetColumnName(StoreObjectIdentifier.Table(name, schema)) == concurrencyColumnName); @@ -87,12 +87,8 @@ public virtual void ProcessModelFinalizing( { entityTypesMissingConcurrencyColumn ??= new Dictionary(); - // store the entity type which is missing the - // concurrency token property, mapped to an example - // property which _is_ mapped to this concurrency token - // column and which will be used later as a template - entityTypesMissingConcurrencyColumn.Add( - entityType, readOnlyProperties.First()); + // store the concurrency token property to be used later as a template + entityTypesMissingConcurrencyColumn.Add(entityType, readOnlyProperties.First()); } } @@ -168,7 +164,17 @@ public virtual void ProcessModelFinalizing( nonHierarchyTypesCount++; } - foreach (var property in entityType.GetDeclaredProperties()) + concurrencyColumns = FindConcurrencyColumns(entityType, storeObject, concurrencyColumns); + } + + return nonHierarchyTypesCount < 2 ? null : concurrencyColumns; + + static Dictionary>? FindConcurrencyColumns( + IReadOnlyTypeBase structuralType, + StoreObjectIdentifier storeObject, + Dictionary>? concurrencyColumns) + { + foreach (var property in structuralType.GetDeclaredProperties()) { if (!property.IsConcurrencyToken || (property.ValueGenerated & ValueGenerated.OnUpdate) == 0) @@ -183,7 +189,6 @@ public virtual void ProcessModelFinalizing( } concurrencyColumns ??= new Dictionary>(); - if (!concurrencyColumns.TryGetValue(columnName, out var properties)) { properties = new List(); @@ -192,9 +197,14 @@ public virtual void ProcessModelFinalizing( properties.Add(property); } - } - return nonHierarchyTypesCount < 2 ? null : concurrencyColumns; + foreach (var complexProperty in structuralType.GetDeclaredComplexProperties()) + { + concurrencyColumns = FindConcurrencyColumns(complexProperty.ComplexType, storeObject, concurrencyColumns); + } + + return concurrencyColumns; + } } /// @@ -209,8 +219,7 @@ public static bool IsConcurrencyTokenMissing( IReadOnlyEntityType entityType, IReadOnlyList mappedTypes) { - if (entityType.FindPrimaryKey() == null - || propertiesMappedToConcurrencyColumn.Count == 0) + if (entityType.FindPrimaryKey() == null) { return false; } @@ -218,20 +227,18 @@ public static bool IsConcurrencyTokenMissing( var propertyMissing = true; foreach (var mappedProperty in propertiesMappedToConcurrencyColumn) { - var declaringType = mappedProperty.DeclaringType; - var declaringEntityType = declaringType as IEntityType; - if (declaringType.IsAssignableFrom(entityType) - || entityType.IsAssignableFrom(declaringType) - || declaringEntityType != null - && (declaringEntityType.IsInOwnershipPath(entityType) - || entityType.IsInOwnershipPath(declaringEntityType))) + var containingEntityType = mappedProperty.DeclaringType.ContainingEntityType; + if (containingEntityType.IsAssignableFrom(entityType) + || entityType.IsAssignableFrom(containingEntityType) + || containingEntityType.IsInOwnershipPath(entityType) + || entityType.IsInOwnershipPath(containingEntityType)) { - // The concurrency token is on the base type, derived type or in the same aggregate + // The concurrency token is on the base type, derived type, a contained complex type or in the same aggregate propertyMissing = false; continue; } - var linkingFks = declaringEntityType?.FindForeignKeys(declaringEntityType.FindPrimaryKey()!.Properties) + var linkingFks = containingEntityType.FindForeignKeys(containingEntityType.FindPrimaryKey()!.Properties) .Where(fk => fk.PrincipalKey.IsPrimaryKey() && mappedTypes.Contains(fk.PrincipalEntityType)).ToList(); if (linkingFks != null @@ -259,9 +266,8 @@ private static void RemoveDerivedEntityTypes( var baseType = entityType.BaseType; while (baseType != null) { - if (entityTypeDictionary.ContainsKey(baseType)) + if (entityTypeDictionary.Remove(entityType)) { - entityTypeDictionary.Remove(entityType); removed = true; break; } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index e3c8d25ea62..0a911611338 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -758,8 +758,10 @@ private static void AddSqlQueries(RelationalModel databaseModel, IEntityType ent var column = sqlQuery.FindColumn(columnName); if (column == null) { - column = new SqlQueryColumn(columnName, property.GetColumnType(mappedQuery), sqlQuery); - column.IsNullable = property.IsColumnNullable(mappedQuery); + column = new SqlQueryColumn(columnName, property.GetColumnType(mappedQuery), sqlQuery) + { + IsNullable = property.IsColumnNullable(mappedQuery) + }; sqlQuery.Columns.Add(columnName, column); } else if (!property.IsColumnNullable(mappedQuery)) 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..62698cfeaae 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,85 @@ 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) + => GetEntry((IInternalEntry)entry, 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) + => GetEntry((IInternalEntry)entry, 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 +272,54 @@ 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 = Entry.SharedIdentityEntry == null + ? GetOriginalProviderValue(Entry, Property) + : GetOriginalProviderValue(Entry.SharedIdentityEntry, Property); + if (Property.GetProviderValueComparer().Equals( + originalValue, + modification.Entry.SharedIdentityEntry == null + ? GetOriginalProviderValue(modification.Entry, modification.Property) + : GetOriginalProviderValue(modification.Entry.SharedIdentityEntry, 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 +331,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/Internal/CommandBatchPreparer.cs b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs index 4b2f86e7288..694c31232b4 100644 --- a/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs +++ b/src/EFCore.Relational/Update/Internal/CommandBatchPreparer.cs @@ -448,7 +448,7 @@ private void Format( var dependentCommand = reverseDependency ? target : source; var dependentEntry = dependentCommand.Entries.First(e => foreignKey.DeclaringEntityType.IsAssignableFrom(e.EntityType)); builder.Append(dependentEntry.BuildCurrentValuesString(foreignKey.Properties)) - .Append(" "); + .Append(' '); if (!reverseDependency) { @@ -507,7 +507,7 @@ private void Format(IKey key, IReadOnlyModificationCommand source, IReadOnlyModi ? dependentEntry.BuildCurrentValuesString(key.Properties) : dependentEntry.BuildOriginalValuesString(key.Properties)); - builder.Append(" "); + builder.Append(' '); if (!reverseDependency) { @@ -1122,15 +1122,15 @@ private void AddUniqueValueEdges() continue; } - var indexValue = ((TableIndex)index).GetRowIndexValueFactory() + var (value, _) = ((TableIndex)index).GetRowIndexValueFactory() .CreateEquatableIndexValue(command, fromOriginalValues: true); - if (indexValue.Value != null) + if (value != null) { indexPredecessorsMap ??= new Dictionary>(); - if (!indexPredecessorsMap.TryGetValue(indexValue.Value, out var predecessorCommands)) + if (!indexPredecessorsMap.TryGetValue(value, out var predecessorCommands)) { predecessorCommands = new List(); - indexPredecessorsMap.Add(indexValue.Value, predecessorCommands); + indexPredecessorsMap.Add(value, predecessorCommands); } predecessorCommands.Add(command); @@ -1204,12 +1204,12 @@ private void AddUniqueValueEdges() continue; } - var indexValue = ((TableIndex)index).GetRowIndexValueFactory() + var (value, hasNullValue) = ((TableIndex)index).GetRowIndexValueFactory() .CreateEquatableIndexValue(command); - if (indexValue.Value != null) + if (value != null) { - AddMatchingPredecessorEdge(indexPredecessorsMap, indexValue.Value, command, - new CommandDependency(index, breakable: index.Filter != null || indexValue.HasNullValue)); + AddMatchingPredecessorEdge(indexPredecessorsMap, value, command, + new CommandDependency(index, breakable: index.Filter != null || hasNullValue)); } } } diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index d54de736ace..501f0dc408e 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -111,7 +111,8 @@ public virtual IReadOnlyList ColumnModifications [EntityFrameworkInternal] public virtual void AssertColumnsNotInitialized() { - if (_columnModifications != null) + if (_columnModifications != null + && !Debugger.IsAttached) { throw new Exception("_columnModifications have been initialized prematurely"); } @@ -249,18 +250,22 @@ 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.Count == 1 + && (_entries[0].SharedIdentityEntry != 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, tableMapping); } else // Stored procedure mapping case { @@ -476,7 +370,7 @@ entry.EntityState is EntityState.Modified or EntityState.Added { if (parameter.FindParameterMapping(entry.EntityType) is { } parameterMapping) { - HandleColumnModification(parameterMapping); + HandleColumn(parameterMapping); continue; } @@ -501,7 +395,7 @@ entry.EntityState is EntityState.Modified or EntityState.Added { if (resultColumn.FindColumnMapping(entry.EntityType) is { } resultColumnMapping) { - HandleColumnModification(resultColumnMapping); + HandleColumn(resultColumnMapping); continue; } @@ -535,7 +429,24 @@ entry.EntityState is EntityState.Modified or EntityState.Added } } - void HandleColumnModification(IColumnMappingBase columnMapping) + void HandleNonJson(ITypeBase structuralType, ITableMapping tableMapping) + { + foreach (var columnMapping in tableMapping.ColumnMappings) + { + HandleColumn(columnMapping); + } + + foreach (var complexProperty in structuralType.GetComplexProperties()) + { + var complexTableMapping = GetTableMapping(complexProperty.ComplexType); + if (complexTableMapping != null) + { + HandleNonJson(complexProperty.ComplexType, complexTableMapping); + } + } + } + + void HandleColumn(IColumnMappingBase columnMapping) { var property = columnMapping.Property; var column = columnMapping.Column; @@ -551,7 +462,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,16 +474,21 @@ void HandleColumnModification(IColumnMappingBase columnMapping) if (adding) { writeValue = property.GetBeforeSaveBehavior() == PropertySaveBehavior.Save; + columnPropagator?.TryPropagate(columnMapping, entry); } - else if (((updating && property.GetAfterSaveBehavior() == PropertySaveBehavior.Save) + else if (storedProcedureParameter is not { ForOriginalValue: true } + && !deleting + && ((updating && property.GetAfterSaveBehavior() == PropertySaveBehavior.Save) || (!isKey && nonMainEntry) - || entry.SharedIdentityEntry != null) - && storedProcedureParameter is not { ForOriginalValue: true }) + || entry.SharedIdentityEntry != null)) { // 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 + || entry.EntityState == EntityState.Deleted + || ColumnModification.IsModified(entry, property) + || StoreStoredProcedure is not null); } } @@ -619,6 +535,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 +545,26 @@ 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()) + { + var complexTableMapping = GetTableMapping(complexProperty.ComplexType); + if (complexTableMapping != null) + { + HandleSharedColumns( + complexProperty.ComplexType, entry, complexTableMapping, deleting, sharedTableColumnMap); + } + } + } + static JsonPartialUpdateInfo? FindJsonPartialUpdateInfo(IUpdateEntry entry, List processedEntries) { var result = new JsonPartialUpdateInfo(); @@ -667,10 +604,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 +657,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 +906,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 +946,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 +956,12 @@ private static void InitializeSharedColumns( continue; } + if (columnMapping.Column.PropertyMappings.Select(p => p.Property).Distinct().Count() == 1 + && entry.SharedIdentityEntry == null) + { + continue; + } + var columnName = columnMapping.Column.Name; if (!columnMap.TryGetValue(columnName, out var columnPropagator)) { @@ -911,7 +969,7 @@ private static void InitializeSharedColumns( columnMap.Add(columnName, columnPropagator); } - if (updating) + if (!deleting) { columnPropagator.RecordValue(columnMapping, entry); } @@ -1022,6 +1080,7 @@ private sealed class ColumnValuePropagator private bool _write; private object? _originalValue; private object? _currentValue; + private bool _originalValueInitialized; public IColumnModification? ColumnModification { get; set; } @@ -1032,20 +1091,31 @@ 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 (_currentValue == null + || !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,12 +1132,20 @@ 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 + if ((property.GetAfterSaveBehavior() == PropertySaveBehavior.Save || entry.EntityState == EntityState.Added) + && property.ValueGenerated != ValueGenerated.Never) { var value = _currentValue; var converter = property.GetTypeMapping().Converter; @@ -1076,7 +1154,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..751a6ac5257 --- /dev/null +++ b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs @@ -0,0 +1,330 @@ +// 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; + +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..a714812a4e6 --- /dev/null +++ b/src/EFCore/ChangeTracking/Internal/InternalComplexEntry.cs @@ -0,0 +1,1177 @@ +// 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 complexEntry in _complexEntries) + { + complexEntry.SetEntityState(EntityState.Modified, acceptChanges, modifyProperties); + } + } + + if (oldState == newState) + { + return; + } + + if (newState == EntityState.Unchanged) + { + _stateData.FlagAllProperties( + ComplexType.PropertyCount, PropertyFlag.Modified, + flagged: false); + + foreach (var complexEntry in _complexEntries) + { + complexEntry.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) + { + Check.DebugAssert(ComplexObject != null || ComplexType.ComplexProperty.IsNullable, + $"Unexpected null for {ComplexType.DisplayName()}"); + return 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) + { + Check.DebugAssert(ComplexObject != null, "null object for " + ComplexType.DisplayName()); + if (propertyBase.IsShadowProperty()) + { + _shadowValues[propertyBase.GetShadowIndex()] = value; + } + else + { + var concretePropertyBase = (IRuntimePropertyBase)propertyBase; + + var setter = forMaterialization + ? concretePropertyBase.MaterializationSetter + : concretePropertyBase.GetSetter(); + + 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) + { + Check.DebugAssert(ComplexObject != null || ComplexType.ComplexProperty.IsNullable, + $"Unexpected null for {ComplexType.DisplayName()}"); + return _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) + { + Check.DebugAssert(ComplexObject != null, "null object for " + ComplexType.DisplayName()); + 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) + { + Check.DebugAssert(ComplexObject != null, "null object for " + ComplexType.DisplayName()); + 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); + + foreach (var complexEntry in _complexEntries) + { + complexEntry.AcceptChanges(); + } + + 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); + } + + foreach (var complexEntry in _complexEntries) + { + complexEntry.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. + /// + 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..67e625a4e20 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); } /// @@ -184,7 +194,7 @@ public async Task SetEntityStateAsync( CancellationToken cancellationToken = default) { var oldState = _stateData.EntityState; - bool adding = PrepareForAdd(entityState); + var adding = PrepareForAdd(entityState); entityState = await PropagateToUnknownKeyAsync( oldState, entityState, adding, forceStateWhenUnknownKey, cancellationToken).ConfigureAwait(false); @@ -313,6 +323,11 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc _stateData.FlagProperty(property.GetIndex(), PropertyFlag.Modified, isFlagged: false); } } + + foreach (var complexEntry in _complexEntries) + { + complexEntry.SetEntityState(EntityState.Modified, acceptChanges, modifyProperties); + } } if (oldState == newState) @@ -325,6 +340,11 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc _stateData.FlagAllProperties( EntityType.PropertyCount, PropertyFlag.Modified, flagged: false); + + foreach (var complexEntry in _complexEntries) + { + complexEntry.SetEntityState(EntityState.Unchanged, acceptChanges, modifyProperties); + } } if (_stateData.EntityState != oldState) @@ -414,7 +434,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 +513,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 +684,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 +801,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 +842,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 +852,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 +861,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 +907,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 +919,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 +937,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 +953,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 +962,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 +1238,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 +1469,11 @@ private void SetProperty( SetIsLoaded(navigation, value != null); } + if (propertyBase is IComplexProperty complexProperty) + { + _complexEntries.SetValue(value, this, complexProperty); + } + StateManager.InternalEntityEntryNotifier.PropertyChanged(this, propertyBase, setModified); } } @@ -1482,6 +1545,11 @@ public void AcceptChanges() _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.IsTemporary, false); _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.Unknown, false); + foreach(var complexEntry in _complexEntries) + { + complexEntry.AcceptChanges(); + } + var currentState = EntityState; switch (currentState) { @@ -1653,7 +1721,7 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool CoreStrings.RelationshipConceptualNullSensitive( foreignKey.PrincipalEntityType.DisplayName(), EntityType.DisplayName(), - this.BuildOriginalValuesString(foreignKey.Properties))); + BuildOriginalValuesString(foreignKey.Properties))); } throw new InvalidOperationException( @@ -1676,7 +1744,7 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool CoreStrings.PropertyConceptualNullSensitive( property.Name, EntityType.DisplayName(), - this.BuildOriginalValuesString(new[] { property }))); + BuildOriginalValuesString(new[] { property }))); } throw new InvalidOperationException( @@ -1700,6 +1768,11 @@ public void DiscardStoreGeneratedValues() _storeGeneratedValues = new SidecarValues(); _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.IsStoreGenerated, false); } + + foreach (var complexEntry in _complexEntries) + { + complexEntry.DiscardStoreGeneratedValues(); + } } /// @@ -1823,7 +1896,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 +1918,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 +2055,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 +2099,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/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index e98815ec490..5913317de23 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -17,7 +17,7 @@ public class StateManager : IStateManager { private readonly EntityReferenceMap _entityReferenceMap = new(hasSubMap: true); - private IDictionary>>? _referencedUntrackedEntities; + private Dictionary>>? _referencedUntrackedEntities; private IIdentityMap? _identityMap0; private IIdentityMap? _identityMap1; private Dictionary? _identityMaps; 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/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 62ada3a7e2a..f93f72d9909 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; -using System.Security.Cryptography; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; 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..e286ec5be4b 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; @@ -221,7 +221,7 @@ public virtual void SetRemovedFromModel() /// 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. /// - new public virtual EntityType? BaseType + public new virtual EntityType? BaseType => (EntityType?)base.BaseType; /// @@ -1828,9 +1828,8 @@ public virtual IEnumerable FindSkipNavigationsInHierarchy(string var removed = _skipNavigations.Remove(navigation.Name); Check.DebugAssert(removed, "Expected the navigation to be removed"); - removed = navigation.ForeignKey is ForeignKey foreignKey - ? foreignKey.ReferencingSkipNavigations!.Remove(navigation) - : true; + removed = navigation.ForeignKey is not ForeignKey foreignKey + || foreignKey.ReferencingSkipNavigations!.Remove(navigation); Check.DebugAssert(removed, "removed is false"); removed = navigation.TargetEntityType.DeclaredReferencingSkipNavigations!.Remove(navigation); @@ -2277,7 +2276,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 +2306,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 => @@ -2959,14 +2958,9 @@ private void CheckDiscriminatorProperty(Property? property) /// /// The name of the property that will be used for storing a discriminator value. public virtual string? GetDiscriminatorPropertyName() - { - if (BaseType != null) - { - return ((IReadOnlyEntityType)this).GetRootType().GetDiscriminatorPropertyName(); - } - - return (string?)this[CoreAnnotationNames.DiscriminatorProperty]; - } + => BaseType != null + ? ((IReadOnlyEntityType)this).GetRootType().GetDiscriminatorPropertyName() + : (string?)this[CoreAnnotationNames.DiscriminatorProperty]; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 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..8c194549d2e 100644 --- a/src/EFCore/Metadata/RuntimeTypeBase.cs +++ b/src/EFCore/Metadata/RuntimeTypeBase.cs @@ -23,16 +23,15 @@ public abstract class RuntimeTypeBase : AnnotatableBase, IRuntimeTypeBase private readonly SortedSet _directlyDerivedTypes = new(TypeBaseNameComparer.Instance); private readonly SortedDictionary _properties; - private readonly SortedDictionary _complexProperties = - new SortedDictionary(StringComparer.Ordinal); + private readonly SortedDictionary _complexProperties = new(StringComparer.Ordinal); private readonly PropertyInfo? _indexerPropertyInfo; private readonly bool _isPropertyBag; 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 +490,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 +514,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 +687,7 @@ PropertyCounts IRuntimeTypeBase.Counts } /// - Func IRuntimeTypeBase.OriginalValuesFactory + Func IRuntimeTypeBase.OriginalValuesFactory => NonCapturingLazyInitializer.EnsureInitialized( ref _originalValuesFactory, this, static complexType => RuntimeFeature.IsDynamicCodeSupported @@ -704,7 +703,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.Cosmos.FunctionalTests/F1CosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs index 5401a33ba3a..3e2668bb365 100644 --- a/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs +++ b/test/EFCore.Cosmos.FunctionalTests/F1CosmosFixture.cs @@ -43,6 +43,7 @@ protected override void BuildModelExternal(ModelBuilder modelBuilder) }); modelBuilder.Entity() + .Ignore(s => s.Details) .OwnsOne( s => s.Details, eb => { diff --git a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs index 553890e9114..1b21da63934 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,79 @@ 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, + Engine = new Engine(), + Operator = new Operator { Name = "Kai Saunders", Details = new OperatorDetails() } + }); + + 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 +444,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 +934,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.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs b/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs index 6a6add4cc3a..eaf6b6f47d0 100644 --- a/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Update/UpdateSqlGeneratorTestBase.cs @@ -244,7 +244,7 @@ protected IModificationCommand CreateInsertCommand(bool identityKey = true, bool entry.SetEntityState(EntityState.Added); var generator = new ParameterNameGenerator(); - var duckType = model.FindEntityType(typeof(Duck)); + var duckType = entry.EntityType; var idProperty = duckType.FindProperty(nameof(Duck.Id)); var nameProperty = duckType.FindProperty(nameof(Duck.Name)); var quacksProperty = duckType.FindProperty(nameof(Duck.Quacks)); @@ -286,7 +286,7 @@ protected IModificationCommand CreateUpdateCommand(bool isComputed = true, bool entry.SetEntityState(EntityState.Modified); var generator = new ParameterNameGenerator(); - var duckType = model.FindEntityType(typeof(Duck)); + var duckType = entry.EntityType; var idProperty = duckType.FindProperty(nameof(Duck.Id)); var nameProperty = duckType.FindProperty(nameof(Duck.Name)); var quacksProperty = duckType.FindProperty(nameof(Duck.Quacks)); @@ -317,13 +317,12 @@ protected IModificationCommand CreateUpdateCommand(bool isComputed = true, bool protected IModificationCommand CreateDeleteCommand(bool concurrencyToken = true) { - var model = GetDuckModel(); - var stateManager = TestHelpers.CreateContextServices(model).GetRequiredService(); + var stateManager = TestHelpers.CreateContextServices(GetDuckModel()).GetRequiredService(); var entry = stateManager.GetOrCreateEntry(new Duck()); entry.SetEntityState(EntityState.Deleted); var generator = new ParameterNameGenerator(); - var duckType = model.FindEntityType(typeof(Duck)); + var duckType = entry.EntityType; var idProperty = duckType.FindProperty(nameof(Duck.Id)); var concurrencyProperty = duckType.FindProperty(nameof(Duck.ConcurrencyToken)); diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs index b567393708e..acd7f8b1eb7 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs @@ -152,6 +152,56 @@ public virtual void Missing_concurrency_token_property_is_created_on_the_sharing Assert.Equal(ValueGenerated.OnAddOrUpdate, concurrencyProperty.ValueGenerated); } + [ConditionalFact] + public virtual void Missing_concurrency_token_property_is_created_on_the_sharing_type_with_complex_property() + { + var modelBuilder = GetModelBuilder(); + modelBuilder.Entity().HasKey(a => a.Id); + modelBuilder.Entity().ToTable(nameof(Animal)); + modelBuilder.Entity(ab => + { + ab.HasKey(a => a.Id); + ab.HasOne(a => a.FavoritePerson).WithOne().HasForeignKey(p => p.Id); + ab.ComplexProperty(a => a.Dwelling) + .Property("Version").IsRowVersion().HasColumnName("Version"); + }); + + var model = modelBuilder.Model; + model.FinalizeModel(); + + var personEntityType = model.FindEntityType(typeof(Person)); + var concurrencyProperty = personEntityType.FindProperty("_TableSharingConcurrencyTokenConvention_Version"); + Assert.True(concurrencyProperty.IsConcurrencyToken); + Assert.True(concurrencyProperty.IsShadowProperty()); + Assert.Equal("Version", concurrencyProperty.GetColumnName()); + Assert.Equal(ValueGenerated.OnAddOrUpdate, concurrencyProperty.ValueGenerated); + + var animalEntityType = model.FindEntityType(typeof(Animal)); + Assert.All(animalEntityType.GetProperties(), p => Assert.NotEqual(typeof(byte[]), p.ClrType)); + } + + [ConditionalFact] + public virtual void Concurrency_token_property_is_not_created_on_the_sharing_when_on_complex_property() + { + var modelBuilder = GetModelBuilder(); + modelBuilder.Entity().HasKey(a => a.Id); + modelBuilder.Entity().ToTable(nameof(Animal)); + modelBuilder.Entity().Property("Version").IsRowVersion().HasColumnName("Version"); + modelBuilder.Entity(ab => + { + ab.HasKey(a => a.Id); + ab.HasOne(a => a.FavoritePerson).WithOne().HasForeignKey(p => p.Id); + ab.ComplexProperty(a => a.Dwelling) + .Property("Version").IsRowVersion().HasColumnName("Version"); + }); + + var model = modelBuilder.Model; + model.FinalizeModel(); + + var animalEntityType = model.FindEntityType(typeof(Animal)); + Assert.All(animalEntityType.GetProperties(), p => Assert.NotEqual(typeof(byte[]), p.ClrType)); + } + protected class Animal { public int Id { get; set; } diff --git a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs index 54b290dbbb1..d0453ce9ed2 100644 --- a/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs +++ b/test/EFCore.Relational.Tests/Update/CommandBatchPreparerTest.cs @@ -851,9 +851,7 @@ public void BatchCommands_creates_batch_on_incomplete_updates_for_shared_table_n if (state == EntityState.Deleted) { - // Detect indirect update dependencies. Issue #17947. - Assert.Throws( - () => Assert.Single(commandBatches)); + Assert.Single(commandBatches); } else { diff --git a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs index 6375c8f448c..e9344938065 100644 --- a/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs +++ b/test/EFCore.Relational.Tests/Update/ReaderModificationCommandBatchTest.cs @@ -37,7 +37,7 @@ public void TryAddCommand_adds_command_if_batch_is_valid() }); var entry2 = CreateEntry(EntityState.Modified); - var property2 = entry1.EntityType.FindProperty("Name")!; + var property2 = entry2.EntityType.FindProperty("Name")!; var command2 = CreateModificationCommand( "T2", null, @@ -53,8 +53,10 @@ public void TryAddCommand_adds_command_if_batch_is_valid() false, true, false, false, true) }); - var batch = new ModificationCommandBatchFake(); - batch.ShouldBeValid = true; + var batch = new ModificationCommandBatchFake + { + ShouldBeValid = true + }; Assert.True(batch.TryAddCommand(command1)); Assert.True(batch.TryAddCommand(command2)); batch.Complete(moreBatchesExpected: false); @@ -97,7 +99,7 @@ public void TryAddCommand_does_not_add_command_batch_is_invalid() }); var entry2 = CreateEntry(EntityState.Modified); - var property2 = entry1.EntityType.FindProperty("Name")!; + var property2 = entry2.EntityType.FindProperty("Name")!; var command2 = CreateModificationCommand( "T2", null, diff --git a/test/EFCore.Specification.Tests/F1FixtureBase.cs b/test/EFCore.Specification.Tests/F1FixtureBase.cs index 95fa9edb64a..81907ab8ffc 100644 --- a/test/EFCore.Specification.Tests/F1FixtureBase.cs +++ b/test/EFCore.Specification.Tests/F1FixtureBase.cs @@ -166,7 +166,15 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder) modelBuilder.Entity( b => { - b.OwnsOne(s => s.Details); + // TODO: Configure as ComplexProperty when optional complex types are supported + // Issue #31376 + b.OwnsOne( + s => s.Details, eb => + { + eb.Property(d => d.Space); + eb.Property("Version").IsRowVersion(); + eb.Property(Sponsor.ClientTokenPropertyName).IsConcurrencyToken(); + }); ConfigureConstructorBinding(b.Metadata); }); @@ -184,15 +192,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/OptimisticConcurrencySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs index a45c0fec087..78b0c53df2b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs @@ -283,12 +283,6 @@ await c.Database.CreateExecutionStrategy().ExecuteAsync( circuitInner.Name += "-"; } - if (mapping == Mapping.Tpc) // Issue #29751. - { - await Assert.ThrowsAsync(() => innerContext.SaveChangesAsync()); - return; - } - await innerContext.SaveChangesAsync(); if (updateDependentFirst && mapping == Mapping.Tpt) // Issue #22060 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; + } } diff --git a/test/EFCore.Sqlite.FunctionalTests/TPTTableSplittingSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/TPTTableSplittingSqliteTest.cs index 2a0a49fedc9..7dcf136b6ca 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TPTTableSplittingSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TPTTableSplittingSqliteTest.cs @@ -10,6 +10,12 @@ public TPTTableSplittingSqliteTest(ITestOutputHelper testOutputHelper) { } + public override Task Can_insert_dependent_with_just_one_parent() + { + // This scenario is not valid for TPT + return Task.CompletedTask; + } + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs index 48d1ec582a2..a94caa63b5d 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/ChangeDetectorTest.cs @@ -1256,7 +1256,6 @@ public void Handles_notification_of_principal_key_change() { var contextServices = CreateContextServices(BuildNotifyingModel()); - var changeDetector = contextServices.GetRequiredService(); var stateManager = contextServices.GetRequiredService(); var category = new NotifyingCategory { Id = -1, PrincipalId = 77 }; @@ -1682,7 +1681,6 @@ public void Brings_in_single_new_entity_on_notification_of_set_on_collection_nav { var contextServices = CreateContextServices(BuildNotifyingModel()); - var changeDetector = contextServices.GetRequiredService(); var stateManager = contextServices.GetRequiredService(); var product1 = new NotifyingProduct { Id = Guid.NewGuid(), DependentId = 77 }; diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs index 43db9739093..4d12d811509 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderGenericTest.cs @@ -3,8 +3,6 @@ #nullable enable -using System.Numerics; -using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable InconsistentNaming diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs index 95491e3608b..d9a779b3924 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderNonGenericTest.cs @@ -3,8 +3,6 @@ #nullable enable -using Microsoft.EntityFrameworkCore.Metadata; - namespace Microsoft.EntityFrameworkCore.ModelBuilding; public class ModelBuilderNonGenericTest : ModelBuilderTest diff --git a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs index 7a6a14f6e3b..900429fc617 100644 --- a/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ModelBuilderTestBase.cs @@ -4,8 +4,6 @@ #nullable enable using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Metadata; -using static Microsoft.EntityFrameworkCore.ModelBuilding.ModelBuilderTest; // ReSharper disable InconsistentNaming namespace Microsoft.EntityFrameworkCore.ModelBuilding;