From 1a93f442ba499f6bb2b3c4f278ab5cd3516597fb Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Thu, 10 Aug 2023 11:32:07 +0100 Subject: [PATCH 1/4] Initial change tracking and DetectChanges for complex types Part of #9906 --- .../Storage/Internal/InMemoryTable.cs | 20 +- .../Update/ColumnModification.cs | 59 +- .../Update/ModificationCommand.cs | 6 +- .../ChangeTracking/Internal/ChangeDetector.cs | 2 +- .../EmptyShadowValuesFactoryFactory.cs | 4 +- .../ChangeTracking/Internal/IInternalEntry.cs | 10 +- .../InternalEntityEntry.ComplexEntries.cs | 62 - ...nternalEntityEntry.InternalComplexEntry.cs | 1177 ---------------- .../InternalEntityEntry.OriginalValues.cs | 6 +- .../Internal/InternalEntityEntry.cs | 61 +- .../Internal/OriginalValuesFactoryFactory.cs | 4 +- .../RelationshipSnapshotFactoryFactory.cs | 4 +- .../Internal/ShadowValuesFactoryFactory.cs | 4 +- .../Internal/SidecarValuesFactoryFactory.cs | 4 +- .../Internal/SnapshotFactoryFactory.cs | 26 +- .../Internal/SnapshotFactoryFactory`.cs | 6 +- .../ChangeTracking/Internal/StateManager.cs | 2 +- src/EFCore/Metadata/IEntityType.cs | 34 + .../Metadata/Internal/ClrAccessorFactory.cs | 2 +- .../Internal/ClrPropertyGetterFactory.cs | 6 +- .../Internal/ClrPropertySetterFactory.cs | 29 +- src/EFCore/Metadata/Internal/ComplexType.cs | 96 -- .../Internal/ComplexTypeExtensions.cs | 58 - .../Metadata/Internal/EntityTypeExtensions.cs | 42 +- .../Metadata/Internal/IRuntimeComplexType.cs | 8 - .../Metadata/Internal/IRuntimeEntityType.cs | 149 +- .../Metadata/Internal/IRuntimeTypeBase.cs | 120 -- .../Internal/PropertyAccessorsFactory.cs | 2 +- src/EFCore/Metadata/Internal/PropertyBase.cs | 21 +- .../Metadata/Internal/PropertyExtensions.cs | 9 + src/EFCore/Metadata/RuntimeComplexType.cs | 12 - src/EFCore/Metadata/RuntimeEntityType.cs | 128 +- src/EFCore/Metadata/RuntimePropertyBase.cs | 2 +- src/EFCore/Metadata/RuntimeTypeBase.cs | 115 -- src/EFCore/Update/UpdateEntryExtensions.cs | 98 +- .../ComplexTypesTrackingInMemoryTest.cs | 51 + .../ComplexTypesTrackingTestBase.cs | 491 +++++++ .../ComplexTypesTrackingSqlServerTest.cs | 26 + .../ComplexTypesTrackingSqliteTest.cs | 26 + test/EFCore.Tests/DbContextServicesTest.cs | 31 + test/EFCore.Tests/DbContextTest.cs | 583 +++++++- test/EFCore.Tests/DbContextTrackingTest.cs | 1248 +++++++++++++++-- .../ModelBuilding/ComplexTypeTestBase.cs | 6 +- 43 files changed, 2865 insertions(+), 1985 deletions(-) delete mode 100644 src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs delete mode 100644 src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs create mode 100644 test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs index 31f421cdf30..2be95605cab 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs @@ -5,6 +5,7 @@ using System.Globalization; using Microsoft.EntityFrameworkCore.InMemory.Internal; using Microsoft.EntityFrameworkCore.InMemory.ValueGeneration.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; @@ -44,7 +45,7 @@ public InMemoryTable( _sensitiveLoggingEnabled = sensitiveLoggingEnabled; _nullabilityCheckEnabled = nullabilityCheckEnabled; _rows = new Dictionary(_keyValueFactory.EqualityComparer); - var properties = entityType.GetProperties().ToList(); + var properties = entityType.GetFlattenedProperties().ToList(); _propertyCount = properties.Count; foreach (var property in properties) @@ -163,7 +164,7 @@ private static List GetKeyComparers(IEnumerable proper /// public virtual void Create(IUpdateEntry entry, IDiagnosticsLogger updateLogger) { - var properties = entry.EntityType.GetProperties().ToList(); + var properties = entry.EntityType.GetFlattenedProperties().ToList(); var row = new object?[properties.Count]; var nullabilityErrors = new List(); @@ -171,7 +172,7 @@ public virtual void Create(IUpdateEntry entry, IDiagnosticsLogger(); for (var index = 0; index < properties.Count; index++) { - IsConcurrencyConflict(entry, properties[index], row[index], concurrencyConflicts); + IsConcurrencyConflict(entry, properties[index], row[properties[index].GetIndex()], concurrencyConflicts); } if (concurrencyConflicts.Count > 0) @@ -266,7 +267,7 @@ public virtual void Update(IUpdateEntry entry, IDiagnosticsLogger(); @@ -274,19 +275,20 @@ public virtual void Update(IUpdateEntry entry, IDiagnosticsLogger 0) diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index 62698cfeaae..a59598a9480 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -178,7 +178,6 @@ 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 @@ -186,7 +185,7 @@ public virtual object? Value /// 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); + => entry.GetOriginalValue(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -195,10 +194,10 @@ public virtual object? Value /// 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); + => entry.GetOriginalProviderValue(property); private void SetOriginalValue(object? value) - => GetEntry((IInternalEntry)Entry!, Property!).SetOriginalValue(Property!, value); + => Entry!.SetOriginalValue(Property!, value); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -207,7 +206,7 @@ private void SetOriginalValue(object? value) /// 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); + => entry.GetCurrentValue(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -216,7 +215,7 @@ private void SetOriginalValue(object? value) /// 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); + => entry.GetCurrentProviderValue(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -225,7 +224,7 @@ private void SetOriginalValue(object? value) /// 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); + => entry.SetStoreGeneratedValue(property, value); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -234,7 +233,7 @@ public static void SetStoreGeneratedValue(IUpdateEntry entry, IProperty property /// 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); + => entry.IsModified(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -243,19 +242,7 @@ public static bool IsModified(IUpdateEntry entry, IProperty property) /// 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. + => entry.IsStoreGenerated(property); /// public virtual string? JsonPath { get; } @@ -275,30 +262,28 @@ public virtual void AddSharedColumnModification(IColumnModification modification 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); + var existingEntry = Entry; + var newEntry = modification.Entry; if (_sensitiveLoggingEnabled) { throw new InvalidOperationException( RelationalStrings.ConflictingRowValuesSensitive( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), Entry.BuildCurrentValuesString(Entry.EntityType.FindPrimaryKey()!.Properties), - GetEntry((IInternalEntry)Entry!, Property).BuildCurrentValuesString(new[] { Property }), + Entry.BuildCurrentValuesString(new[] { Property }), newEntry.BuildCurrentValuesString(new[] { modification.Property }), ColumnName)); } throw new InvalidOperationException( RelationalStrings.ConflictingRowValues( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), new[] { Property }.Format(), new[] { modification.Property }.Format(), ColumnName)); -#pragma warning restore EF1001 // Internal EF Core API usage. } if (UseOriginalValueParameter) @@ -331,15 +316,14 @@ 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); + var existingEntry = Entry; + var newEntry = modification.Entry; if (_sensitiveLoggingEnabled) { throw new InvalidOperationException( RelationalStrings.ConflictingOriginalRowValuesSensitive( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), Entry.BuildCurrentValuesString(Entry.EntityType.FindPrimaryKey()!.Properties), existingEntry.BuildOriginalValuesString(new[] { Property }), newEntry.BuildOriginalValuesString(new[] { modification.Property }), @@ -348,12 +332,11 @@ public virtual void AddSharedColumnModification(IColumnModification modification throw new InvalidOperationException( RelationalStrings.ConflictingOriginalRowValues( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), new[] { Property }.Format(), new[] { modification.Property }.Format(), ColumnName)); -#pragma warning restore EF1001 // Internal EF Core API usage. } } diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index e154e96d609..1e2703cc417 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -604,7 +604,7 @@ void HandleSharedColumns( result.Path.Insert(0, pathEntry); } - var modifiedMembers = entry.EntityType.GetProperties().Where(entry.IsModified).ToList(); + var modifiedMembers = entry.EntityType.GetFlattenedProperties().Where(entry.IsModified).ToList(); if (modifiedMembers.Count == 1) { result.Property = modifiedMembers[0]; @@ -854,7 +854,7 @@ private void WriteJson( #pragma warning restore EF1001 // Internal EF Core API usage. writer.WriteStartObject(); - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.IsKey()) { @@ -1108,7 +1108,7 @@ public void RecordValue(IColumnMapping mapping, IUpdateEntry entry) { _currentValue = Update.ColumnModification.GetCurrentProviderValue(entry, property); } - + _write = !_originalValueInitialized || !mapping.Column.ProviderValueComparer.Equals(_originalValue, _currentValue); diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 79f7abdbf8d..1f1c141aa93 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -226,7 +226,7 @@ private bool LocalDetectChanges(InternalEntityEntry entry) OnDetectingEntityChanges(entry); - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.GetOriginalValueIndex() >= 0 && !entry.IsModified(property) diff --git a/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs index d5b61954ce0..b30f0f8ba22 100644 --- a/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.ShadowPropertyCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.ShadowPropertyCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs index 751a6ac5257..bd973bb81df 100644 --- a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs @@ -27,7 +27,7 @@ public interface IInternalEntry /// any release. You should only use it directly in 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; } + IRuntimeEntityType EntityType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -296,14 +296,6 @@ void SetEntityState( 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 diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs deleted file mode 100644 index 84035910e55..00000000000 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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/InternalEntityEntry.InternalComplexEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs deleted file mode 100644 index a714812a4e6..00000000000 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs +++ /dev/null @@ -1,1177 +0,0 @@ -// 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.OriginalValues.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.OriginalValues.cs index 3b4f2915c05..fd3b1581ea5 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.OriginalValues.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.OriginalValues.cs @@ -13,7 +13,7 @@ private readonly struct OriginalValues public OriginalValues(IInternalEntry entry) { - _values = entry.StructuralType.OriginalValuesFactory(entry); + _values = entry.EntityType.OriginalValuesFactory(entry); } public object? GetValue(IInternalEntry entry, IProperty property) @@ -72,7 +72,7 @@ public void RejectChanges(IInternalEntry entry) return; } - foreach (var property in entry.StructuralType.GetProperties()) + foreach (var property in entry.EntityType.GetFlattenedProperties()) { var index = property.GetOriginalValueIndex(); if (index >= 0) @@ -89,7 +89,7 @@ public void AcceptChanges(IInternalEntry entry) return; } - foreach (var property in entry.StructuralType.GetProperties()) + foreach (var property in entry.EntityType.GetFlattenedProperties()) { var index = property.GetOriginalValueIndex(); if (index >= 0) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 2b16492093b..f7c233678a3 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -24,7 +24,6 @@ public sealed partial class InternalEntityEntry : IUpdateEntry, IInternalEntry 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 @@ -42,9 +41,8 @@ public InternalEntityEntry( Entity = entity; _shadowValues = EntityType.EmptyShadowValuesFactory(); _stateData = new StateData(EntityType.PropertyCount, EntityType.NavigationCount); - _complexEntries = new ComplexEntries(this); - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.IsShadowProperty()) { @@ -70,8 +68,6 @@ 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); } /// @@ -296,7 +292,7 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc && newState != EntityState.Detached) { // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.IsKey() && HasTemporaryValue(property)) { @@ -316,18 +312,13 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.Modified, flagged: true); // Hot path; do not use LINQ - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { 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) @@ -340,11 +331,6 @@ 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) @@ -705,8 +691,7 @@ public void OnComplexPropertyModified(IComplexProperty property, bool isModified } else if (currentState == EntityState.Modified && !isModified - && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified) - && _complexEntries.All(e => e.EntityState == EntityState.Unchanged)) + && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified)) { _stateData.EntityState = EntityState.Unchanged; } @@ -1238,15 +1223,6 @@ 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 @@ -1469,11 +1445,6 @@ private void SetProperty( SetIsLoaded(navigation, value != null); } - if (propertyBase is IComplexProperty complexProperty) - { - _complexEntries.SetValue(value, this, complexProperty); - } - StateManager.InternalEntityEntryNotifier.PropertyChanged(this, propertyBase, setModified); } } @@ -1526,7 +1497,7 @@ public void AcceptChanges() { if (!_storeGeneratedValues.IsEmpty) { - foreach (var property in EntityType.GetProperties()) + foreach (var property in EntityType.GetFlattenedProperties()) { var storeGeneratedIndex = property.GetStoreGeneratedIndex(); if (storeGeneratedIndex != -1 @@ -1545,11 +1516,6 @@ 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) { @@ -1581,7 +1547,7 @@ public InternalEntityEntry PrepareToSave() if (EntityState == EntityState.Added) { - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.GetBeforeSaveBehavior() == PropertySaveBehavior.Throw && !HasTemporaryValue(property) @@ -1609,7 +1575,7 @@ public InternalEntityEntry PrepareToSave() } else if (EntityState == EntityState.Modified) { - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Throw && IsModified(property)) @@ -1625,7 +1591,7 @@ public InternalEntityEntry PrepareToSave() } else if (EntityState == EntityState.Deleted) { - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { CheckForUnknownKey(property); } @@ -1731,7 +1697,7 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool } else { - var property = EntityType.GetProperties().FirstOrDefault( + var property = EntityType.GetFlattenedProperties().FirstOrDefault( p => (EntityState != EntityState.Modified || IsModified(p)) && _stateData.IsPropertyFlagged(p.GetIndex(), PropertyFlag.Null)); @@ -1768,11 +1734,6 @@ public void DiscardStoreGeneratedValues() _storeGeneratedValues = new SidecarValues(); _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.IsStoreGenerated, false); } - - foreach (var complexEntry in _complexEntries) - { - complexEntry.DiscardStoreGeneratedValues(); - } } /// @@ -1931,7 +1892,7 @@ private static IEnumerable GetNotificationProperties( { if (string.IsNullOrEmpty(propertyName)) { - foreach (var property in entityType.GetProperties() + foreach (var property in entityType.GetFlattenedProperties() .Where(p => p.GetAfterSaveBehavior() == PropertySaveBehavior.Save)) { yield return property; @@ -2099,7 +2060,7 @@ public DebugView DebugView IEntityType IUpdateEntry.EntityType => EntityType; - IRuntimeTypeBase IInternalEntry.StructuralType + IRuntimeEntityType IInternalEntry.EntityType => EntityType; object IInternalEntry.Object diff --git a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs index f0a15b610de..ebc0663ccdc 100644 --- a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.OriginalValueCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.OriginalValueCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs index 8c4bb0e69f8..548aceb5267 100644 --- a/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.RelationshipPropertyCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.RelationshipPropertyCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs index 02a03f14ae4..579768ad387 100644 --- a/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.ShadowPropertyCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.ShadowPropertyCount; /// /// 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 d43f8eba4b1..e0f758c815e 100644 --- a/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.StoreGeneratedCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.StoreGeneratedCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index 49a7381720f..afb30d4717c 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -21,11 +21,11 @@ public abstract class SnapshotFactoryFactory /// any release. You should only use it directly in 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 CreateEmpty(IRuntimeTypeBase typeBase) - => GetPropertyCount(typeBase) == 0 + public virtual Func CreateEmpty(IRuntimeEntityType entityType) + => GetPropertyCount(entityType) == 0 ? (() => Snapshot.Empty) : Expression.Lambda>( - CreateConstructorExpression(typeBase, null!)) + CreateConstructorExpression(entityType, null!)) .Compile(); /// @@ -35,15 +35,15 @@ public virtual Func CreateEmpty(IRuntimeTypeBase typeBase) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual Expression CreateConstructorExpression( - IRuntimeTypeBase typeBase, + IRuntimeEntityType entityType, ParameterExpression? parameter) { - var count = GetPropertyCount(typeBase); + var count = GetPropertyCount(entityType); var types = new Type[count]; var propertyBases = new IPropertyBase?[count]; - foreach (var propertyBase in typeBase.GetSnapshottableMembers()) + foreach (var propertyBase in entityType.GetSnapshottableMembers()) { var index = GetPropertyIndex(propertyBase); if (index >= 0) @@ -62,7 +62,7 @@ protected virtual Expression CreateConstructorExpression( { snapshotExpressions.Add( CreateSnapshotExpression( - typeBase.ClrType, + entityType.ClrType, parameter, types.Skip(i).Take(Snapshot.MaxGenericTypes).ToArray(), propertyBases.Skip(i).Take(Snapshot.MaxGenericTypes).ToList())); @@ -77,7 +77,7 @@ protected virtual Expression CreateConstructorExpression( } else { - constructorExpression = CreateSnapshotExpression(typeBase.ClrType, parameter, types, propertyBases); + constructorExpression = CreateSnapshotExpression(entityType.ClrType, parameter, types, propertyBases); } return constructorExpression; @@ -119,6 +119,12 @@ protected virtual Expression CreateSnapshotExpression( continue; } + if (propertyBase is IComplexProperty complexProperty) + { + arguments[i] = CreateSnapshotValueExpression(CreateReadValueExpression(parameter, complexProperty), complexProperty); + continue; + } + if (propertyBase.IsShadowProperty()) { arguments[i] = CreateSnapshotValueExpression(CreateReadShadowValueExpression(parameter, propertyBase), propertyBase); @@ -243,7 +249,7 @@ protected virtual Expression CreateReadValueExpression( => Expression.Call( parameter, InternalEntityEntry.MakeGetCurrentValueMethod(property.ClrType), - Expression.Constant(property, typeof(IProperty))); + Expression.Constant(property, typeof(IPropertyBase))); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -259,7 +265,7 @@ protected virtual Expression CreateReadValueExpression( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected abstract int GetPropertyCount(IRuntimeTypeBase typeBase); + protected abstract int GetPropertyCount(IRuntimeEntityType entityType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs index 66f8fc1c536..08328f92b27 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs @@ -19,9 +19,9 @@ public abstract class SnapshotFactoryFactory : SnapshotFactoryFactory /// any release. You should only use it directly in 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 Create(IRuntimeTypeBase typeBase) + public virtual Func Create(IRuntimeEntityType entityType) { - if (GetPropertyCount(typeBase) == 0) + if (GetPropertyCount(entityType) == 0) { return _ => Snapshot.Empty; } @@ -29,7 +29,7 @@ public virtual Func Create(IRuntimeTypeBase typeBase) var parameter = Expression.Parameter(typeof(TInput), "source"); return Expression.Lambda>( - CreateConstructorExpression(typeBase, parameter), + CreateConstructorExpression(entityType, parameter), parameter) .Compile(); } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 5913317de23..8982cf5f077 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -278,7 +278,7 @@ public virtual InternalEntityEntry CreateEntry(IDictionary valu var runtimeEntityType = (IRuntimeEntityType)entityType; var valuesArray = new object?[runtimeEntityType.PropertyCount]; var shadowPropertyValuesArray = new object?[runtimeEntityType.ShadowPropertyCount]; - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { valuesArray[i++] = values.TryGetValue(property.Name, out var value) ? value diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 0d334942a20..6a02f319336 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -486,6 +486,40 @@ public interface IEntityType : IReadOnlyEntityType, ITypeBase /// new IEnumerable GetDeclaredTriggers(); + /// + /// Returns all properties, including those on complex types. + /// + /// The properties. + IEnumerable GetFlattenedProperties() + { + foreach (var property in GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(GetComplexProperties())) + { + yield return property; + } + + IEnumerable ReturnComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(complexType.GetComplexProperties())) + { + yield return property; + } + } + } + } + internal const DynamicallyAccessedMemberTypes DynamicallyAccessedMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors diff --git a/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs b/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs index 246bebc6539..3d3d7c8e790 100644 --- a/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs @@ -44,7 +44,7 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert { var boundMethod = propertyBase != null ? GenericCreate.MakeGenericMethod( - propertyBase.DeclaringType.ClrType, + propertyBase.DeclaringType.ContainingEntityType.ClrType, propertyBase.ClrType, propertyBase.ClrType.UnwrapNullableType()) : GenericCreate.MakeGenericMethod( diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs index b1c1e83f580..8e0eec325f4 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs @@ -32,10 +32,12 @@ protected override IClrPropertyGetter CreateGeneric(setter) : new ClrPropertySetter(setter); - Expression CreateMemberAssignment(Expression parameter) - => propertyBase?.IsIndexerProperty() == true + Expression CreateMemberAssignment(IPropertyBase? property, Expression typeParameter) + { + var targetStructuralType = typeParameter; + if (property?.DeclaringType is IComplexType complexType) + { + targetStructuralType = PropertyBase.CreateMemberAccess( + complexType.ComplexProperty, + typeParameter, + complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false)); + } + + return propertyBase?.IsIndexerProperty() == true ? Expression.Assign( Expression.MakeIndex( - entityParameter, (PropertyInfo)memberInfo, new List { Expression.Constant(propertyBase.Name) }), + targetStructuralType, (PropertyInfo)memberInfo, new List { Expression.Constant(propertyBase.Name) }), convertedParameter) - : Expression.MakeMemberAccess(parameter, memberInfo).Assign(convertedParameter); + : Expression.MakeMemberAccess(targetStructuralType, memberInfo).Assign(convertedParameter); + } } } diff --git a/src/EFCore/Metadata/Internal/ComplexType.cs b/src/EFCore/Metadata/Internal/ComplexType.cs index 6c22afea45b..bdd17fc02d1 100644 --- a/src/EFCore/Metadata/Internal/ComplexType.cs +++ b/src/EFCore/Metadata/Internal/ComplexType.cs @@ -22,17 +22,10 @@ public class ComplexType : TypeBase, IMutableComplexType, IConventionComplexType private ConfigurationSource? _serviceOnlyConstructorBindingConfigurationSource; // Warning: Never access these fields directly as access needs to be thread-safe - private PropertyCounts? _counts; - // _serviceOnlyConstructorBinding needs to be set as well whenever _constructorBinding is set private InstantiationBinding? _constructorBinding; private InstantiationBinding? _serviceOnlyConstructorBinding; - private Func? _originalValuesFactory; - private Func? _temporaryValuesFactory; - private Func? _storeGeneratedValuesFactory; - private Func? _shadowValuesFactory; - private Func? _emptyShadowValuesFactory; private IProperty[]? _foreignKeyProperties; private IProperty[]? _valueGeneratingProperties; @@ -362,95 +355,6 @@ public override IEnumerable FindMembersInHierarchy(string name) => FindPropertiesInHierarchy(name) .Concat(FindComplexPropertiesInHierarchy(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 virtual PropertyCounts Counts - => NonCapturingLazyInitializer.EnsureInitialized( - ref _counts, this, static complexType => - { - complexType.EnsureReadOnly(); - return complexType.CalculateCounts(); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 - => NonCapturingLazyInitializer.EnsureInitialized( - ref _originalValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new OriginalValuesFactoryFactory().Create(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 StoreGeneratedValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _storeGeneratedValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new StoreGeneratedValuesFactoryFactory().CreateEmpty(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 - => NonCapturingLazyInitializer.EnsureInitialized( - ref _temporaryValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new TemporaryValuesFactoryFactory().Create(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 ShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _shadowValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new ShadowValuesFactoryFactory().Create(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 EmptyShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _emptyShadowValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new EmptyShadowValuesFactoryFactory().CreateEmpty(complexType); - }); - /// /// This is an internal API that supports the 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/ComplexTypeExtensions.cs b/src/EFCore/Metadata/Internal/ComplexTypeExtensions.cs index 54d85423508..302ed0e455b 100644 --- a/src/EFCore/Metadata/Internal/ComplexTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/ComplexTypeExtensions.cs @@ -26,62 +26,4 @@ public static bool UseEagerSnapshots(this IReadOnlyComplexType complexType) return changeTrackingStrategy == ChangeTrackingStrategy.Snapshot || changeTrackingStrategy == ChangeTrackingStrategy.ChangedNotifications; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 PropertyCounts CalculateCounts(this IRuntimeComplexType complexType) - { - var propertyIndex = 0; - var complexPropertyIndex = 0; - var originalValueIndex = 0; - var shadowIndex = 0; - var storeGenerationIndex = 0; - var relationshipIndex = ((IRuntimeTypeBase)complexType.ComplexProperty.DeclaringType).Counts.RelationshipCount; - - var baseCounts = (complexType as ComplexType)?.BaseType?.Counts; - if (baseCounts != null) - { - propertyIndex = baseCounts.PropertyCount; - originalValueIndex = baseCounts.OriginalValueCount; - shadowIndex = baseCounts.ShadowCount; - storeGenerationIndex = baseCounts.StoreGeneratedCount; - } - - foreach (var property in complexType.GetProperties()) - { - var indexes = new PropertyIndexes( - index: propertyIndex++, - originalValueIndex: property.RequiresOriginalValue() ? originalValueIndex++ : -1, - shadowIndex: property.IsShadowProperty() ? shadowIndex++ : -1, - relationshipIndex: property.IsKey() || property.IsForeignKey() ? relationshipIndex++ : -1, - storeGenerationIndex: property.MayBeStoreGenerated() ? storeGenerationIndex++ : -1); - - ((IRuntimePropertyBase)property).PropertyIndexes = indexes; - } - - foreach (var complexProperty in complexType.GetComplexProperties()) - { - var indexes = new PropertyIndexes( - index: complexPropertyIndex++, - originalValueIndex: -1, - shadowIndex: complexProperty.IsShadowProperty() ? shadowIndex++ : -1, - relationshipIndex: -1, - storeGenerationIndex: -1); - - ((IRuntimePropertyBase)complexProperty).PropertyIndexes = indexes; - } - - return new PropertyCounts( - propertyIndex, - navigationCount: 0, - complexPropertyIndex, - originalValueIndex, - shadowIndex, - relationshipCount: 0, - storeGenerationIndex); - } } diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs index 841abb56138..7515d9a720a 100644 --- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs @@ -191,17 +191,7 @@ public static PropertyCounts CalculateCounts(this IRuntimeEntityType entityType) ((IRuntimePropertyBase)property).PropertyIndexes = indexes; } - foreach (var complexProperty in entityType.GetDeclaredComplexProperties()) - { - var indexes = new PropertyIndexes( - index: complexPropertyIndex++, - originalValueIndex: -1, - shadowIndex: complexProperty.IsShadowProperty() ? shadowIndex++ : -1, - relationshipIndex: -1, - storeGenerationIndex: -1); - - ((IRuntimePropertyBase)complexProperty).PropertyIndexes = indexes; - } + CountComplexProperties(entityType.GetDeclaredComplexProperties()); var isNotifying = entityType.GetChangeTrackingStrategy() != ChangeTrackingStrategy.Snapshot; @@ -238,6 +228,36 @@ public static PropertyCounts CalculateCounts(this IRuntimeEntityType entityType) shadowIndex, relationshipIndex, storeGenerationIndex); + + void CountComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + var indexes = new PropertyIndexes( + index: complexPropertyIndex++, + originalValueIndex: -1, + shadowIndex: complexProperty.IsShadowProperty() ? shadowIndex++ : -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + + ((IRuntimePropertyBase)complexProperty).PropertyIndexes = indexes; + + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + var complexIndexes = new PropertyIndexes( + index: propertyIndex++, + originalValueIndex: property.RequiresOriginalValue() ? originalValueIndex++ : -1, + shadowIndex: property.IsShadowProperty() ? shadowIndex++ : -1, + relationshipIndex: property.IsKey() || property.IsForeignKey() ? relationshipIndex++ : -1, + storeGenerationIndex: property.MayBeStoreGenerated() ? storeGenerationIndex++ : -1); + + ((IRuntimePropertyBase)property).PropertyIndexes = complexIndexes; + } + + CountComplexProperties(complexType.GetComplexProperties()); + } + } } /// diff --git a/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs b/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs index 272627b3619..a601420ab85 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs @@ -11,12 +11,4 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// public interface IRuntimeComplexType : IComplexType, IRuntimeTypeBase { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IEnumerable IRuntimeTypeBase.GetSnapshottableMembers() - => GetProperties(); } diff --git a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs index 1bfb9b0d5e8..c8d93b191a1 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs @@ -34,6 +34,151 @@ public interface IRuntimeEntityType : IEntityType, IRuntimeTypeBase /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IEnumerable IRuntimeTypeBase.GetSnapshottableMembers() - => GetProperties().Concat(GetNavigations()); + IEnumerable GetSnapshottableMembers() + { + foreach (var property in GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(GetComplexProperties())) + { + yield return property; + } + + IEnumerable ReturnComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + yield return complexProperty; + + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(complexType.GetComplexProperties())) + { + yield return property; + } + } + } + + foreach (var navigation in GetNavigations()) + { + yield return navigation; + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + PropertyCounts Counts { 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. + /// + int OriginalValueCount + => Counts.OriginalValueCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 PropertyCount + => Counts.PropertyCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 ShadowPropertyCount + => Counts.ShadowCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 StoreGeneratedCount + => Counts.StoreGeneratedCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 RelationshipPropertyCount + => Counts.RelationshipCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 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 + /// any release. You should only use it directly in 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; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 StoreGeneratedValuesFactory { 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. + /// + Func TemporaryValuesFactory { 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. + /// + Func ShadowValuesFactory { 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. + /// + Func EmptyShadowValuesFactory { get; } } diff --git a/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs b/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs index 43fcd3992c6..35dd52bffcf 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs @@ -13,117 +13,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// public interface IRuntimeTypeBase : ITypeBase { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 StoreGeneratedValuesFactory { 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. - /// - Func TemporaryValuesFactory { 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. - /// - Func ShadowValuesFactory { 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. - /// - Func EmptyShadowValuesFactory { 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. - /// - PropertyCounts Counts { 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. - /// - int OriginalValueCount - => Counts.OriginalValueCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 PropertyCount - => Counts.PropertyCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 ShadowPropertyCount - => Counts.ShadowCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 StoreGeneratedCount - => Counts.StoreGeneratedCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 RelationshipPropertyCount - => Counts.RelationshipCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 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 @@ -139,13 +28,4 @@ int ComplexPropertyCount /// doing so can result in application failures when updating to a new Entity Framework Core release. /// ConfigurationSource? GetServiceOnlyConstructorBindingConfigurationSource(); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IEnumerable GetSnapshottableMembers() - => throw new NotImplementedException(); } diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 4152e5ed1ff..c80aaf67e52 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -46,7 +46,7 @@ private static Func CreateCurrentValueGetter { property.EnsureReadOnly(); - var _ = ((IRuntimeTypeBase)property.DeclaringType).Counts; + _ = ((IRuntimeEntityType)(((IRuntimeTypeBase)property.DeclaringType).ContainingEntityType)).Counts; }); set => NonCapturingLazyInitializer.EnsureInitialized(ref _indexes, value); @@ -445,6 +445,25 @@ public static Expression CreateMemberAccess( return expression; } + if (property?.DeclaringType is IComplexType complexType) + { + instanceExpression = CreateMemberAccess( + complexType.ComplexProperty, + instanceExpression, + complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false)); + + var instanceVariable = Expression.Variable(instanceExpression.Type, "instance"); + var block = Expression.Block( + new[] { instanceVariable }, + Expression.Assign(instanceVariable, instanceExpression), + Expression.Condition( + Expression.ReferenceEqual(instanceVariable, Expression.Constant(null)), + Expression.Default(memberInfo.GetMemberType()), + Expression.MakeMemberAccess(instanceVariable, memberInfo))); + + return block; + } + return Expression.MakeMemberAccess(instanceExpression, memberInfo); } diff --git a/src/EFCore/Metadata/Internal/PropertyExtensions.cs b/src/EFCore/Metadata/Internal/PropertyExtensions.cs index 325ecb4584b..2bbfb3bcceb 100644 --- a/src/EFCore/Metadata/Internal/PropertyExtensions.cs +++ b/src/EFCore/Metadata/Internal/PropertyExtensions.cs @@ -164,4 +164,13 @@ public static bool RequiresOriginalValue(this IReadOnlyProperty property) || property.IsKey() || property.IsForeignKey() || property.IsUniqueIndex(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 RequiresOriginalValue(this IReadOnlyComplexProperty property) + => property.ComplexType.ContainingEntityType.GetChangeTrackingStrategy() != ChangeTrackingStrategy.ChangingAndChangedNotifications; } diff --git a/src/EFCore/Metadata/RuntimeComplexType.cs b/src/EFCore/Metadata/RuntimeComplexType.cs index abc1633db47..7dee1893ddd 100644 --- a/src/EFCore/Metadata/RuntimeComplexType.cs +++ b/src/EFCore/Metadata/RuntimeComplexType.cs @@ -18,9 +18,6 @@ public class RuntimeComplexType : RuntimeTypeBase, IRuntimeComplexType private InstantiationBinding? _constructorBinding; private InstantiationBinding? _serviceOnlyConstructorBinding; - // Warning: Never access these fields directly as access needs to be thread-safe - private PropertyCounts? _counts; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -138,15 +135,6 @@ public virtual InstantiationBinding? ServiceOnlyConstructorBinding set => _serviceOnlyConstructorBinding = 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. - /// - protected override PropertyCounts Counts - => NonCapturingLazyInitializer.EnsureInitialized(ref _counts, this, static complexType => complexType.CalculateCounts()); - /// /// Returns a string that represents the current object. /// diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 1dd442c4aa9..b409c61baa0 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -50,10 +50,14 @@ private readonly SortedDictionary _triggers // Warning: Never access these fields directly as access needs to be thread-safe private PropertyCounts? _counts; - private Func? _relationshipSnapshotFactory; private IProperty[]? _foreignKeyProperties; private IProperty[]? _valueGeneratingProperties; + private Func? _originalValuesFactory; + private Func? _temporaryValuesFactory; + private Func? _storeGeneratedValuesFactory; + private Func? _shadowValuesFactory; + private Func? _emptyShadowValuesFactory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -791,7 +795,7 @@ public virtual InstantiationBinding? ServiceOnlyConstructorBinding /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override PropertyCounts Counts + public virtual PropertyCounts Counts => NonCapturingLazyInitializer.EnsureInitialized(ref _counts, this, static entityType => entityType.CalculateCounts()); /// @@ -1246,4 +1250,124 @@ IEnumerable IEntityType.GetServiceProperties() PropertyAccessMode IReadOnlyEntityType.GetNavigationAccessMode() => throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetOriginalValuesFactory(Func factory) + => _originalValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetStoreGeneratedValuesFactory(Func factory) + => _storeGeneratedValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetTemporaryValuesFactory(Func factory) + => _temporaryValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetShadowValuesFactory(Func factory) + => _shadowValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetEmptyShadowValuesFactory(Func factory) + => _emptyShadowValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func OriginalValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _originalValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new OriginalValuesFactoryFactory().Create(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func StoreGeneratedValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _storeGeneratedValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new StoreGeneratedValuesFactoryFactory().CreateEmpty(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func TemporaryValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _temporaryValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new TemporaryValuesFactoryFactory().Create(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func ShadowValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _shadowValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new ShadowValuesFactoryFactory().Create(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func EmptyShadowValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _emptyShadowValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new EmptyShadowValuesFactoryFactory().CreateEmpty(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); } diff --git a/src/EFCore/Metadata/RuntimePropertyBase.cs b/src/EFCore/Metadata/RuntimePropertyBase.cs index 1a10e57e8de..c36c24cd4f0 100644 --- a/src/EFCore/Metadata/RuntimePropertyBase.cs +++ b/src/EFCore/Metadata/RuntimePropertyBase.cs @@ -116,7 +116,7 @@ PropertyIndexes IRuntimePropertyBase.PropertyIndexes ref _indexes, this, static property => { - var _ = ((IRuntimeTypeBase)property.DeclaringType).Counts; + _ = ((IRuntimeEntityType)((IRuntimeTypeBase)property.DeclaringType).ContainingEntityType).Counts; }); set => NonCapturingLazyInitializer.EnsureInitialized(ref _indexes, value); } diff --git a/src/EFCore/Metadata/RuntimeTypeBase.cs b/src/EFCore/Metadata/RuntimeTypeBase.cs index b54adbc18a2..a96c92f059a 100644 --- a/src/EFCore/Metadata/RuntimeTypeBase.cs +++ b/src/EFCore/Metadata/RuntimeTypeBase.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; @@ -29,13 +27,6 @@ public abstract class RuntimeTypeBase : AnnotatableBase, IRuntimeTypeBase 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? _storeGeneratedValuesFactory; - private Func? _shadowValuesFactory; - private Func? _emptyShadowValuesFactory; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -134,15 +125,6 @@ protected virtual IEnumerable GetDerivedTypes() return derivedTypes; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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] - protected abstract PropertyCounts Counts { get; } - /// /// Adds a property to this entity type. /// @@ -487,56 +469,6 @@ private IEnumerable FindDerivedComplexProperties(string /// public abstract IEnumerable FindMembersInHierarchy(string 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. - /// - [EntityFrameworkInternal] - public virtual void SetOriginalValuesFactory(Func factory) - => _originalValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetStoreGeneratedValuesFactory(Func factory) - => _storeGeneratedValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetTemporaryValuesFactory(Func factory) - => _temporaryValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetShadowValuesFactory(Func factory) - => _shadowValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetEmptyShadowValuesFactory(Func factory) - => _emptyShadowValuesFactory = factory; - /// /// Gets or sets the for the preferred constructor. /// @@ -672,53 +604,6 @@ IEnumerable IReadOnlyTypeBase.GetDerivedComplexPropert IReadOnlyComplexProperty? IReadOnlyTypeBase.FindDeclaredComplexProperty(string name) => FindDeclaredComplexProperty(name); - /// - PropertyCounts IRuntimeTypeBase.Counts - { - [DebuggerStepThrough] - get => Counts; - } - - /// - Func IRuntimeTypeBase.OriginalValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _originalValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new OriginalValuesFactoryFactory().Create(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.StoreGeneratedValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _storeGeneratedValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new StoreGeneratedValuesFactoryFactory().CreateEmpty(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.TemporaryValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _temporaryValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new TemporaryValuesFactoryFactory().Create(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.ShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _shadowValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new ShadowValuesFactoryFactory().Create(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.EmptyShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _emptyShadowValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new EmptyShadowValuesFactoryFactory().CreateEmpty(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - /// [DebuggerStepThrough] ChangeTrackingStrategy IReadOnlyTypeBase.GetChangeTrackingStrategy() diff --git a/src/EFCore/Update/UpdateEntryExtensions.cs b/src/EFCore/Update/UpdateEntryExtensions.cs index 33efe07bb6e..9e06e370a2d 100644 --- a/src/EFCore/Update/UpdateEntryExtensions.cs +++ b/src/EFCore/Update/UpdateEntryExtensions.cs @@ -124,57 +124,77 @@ public static string ToDebugString( if ((options & ChangeTrackerDebugStringOptions.IncludeProperties) != 0) { - foreach (var property in entry.EntityType.GetProperties()) + DumpProperties(entry.EntityType, indent + 2); + + void DumpProperties(ITypeBase structuralType, int tempIndent) { - builder.AppendLine().Append(indentString); + var tempIndentString = new string(' ', tempIndent); + foreach (var property in structuralType.GetProperties()) + { + builder.AppendLine().Append(tempIndentString); - var currentValue = entry.GetCurrentValue(property); - builder - .Append(" ") - .Append(property.Name) - .Append(": "); + var currentValue = entry.GetCurrentValue(property); + builder + .Append(" ") + .Append(property.Name) + .Append(": "); - AppendValue(currentValue); + AppendValue(currentValue); - if (property.IsPrimaryKey()) - { - builder.Append(" PK"); - } - else if (property.IsKey()) - { - builder.Append(" AK"); - } + if (property.IsPrimaryKey()) + { + builder.Append(" PK"); + } + else if (property.IsKey()) + { + builder.Append(" AK"); + } - if (property.IsForeignKey()) - { - builder.Append(" FK"); - } + if (property.IsForeignKey()) + { + builder.Append(" FK"); + } - if (entry.IsModified(property)) - { - builder.Append(" Modified"); - } + if (entry.IsModified(property)) + { + builder.Append(" Modified"); + } - if (entry.HasTemporaryValue(property)) - { - builder.Append(" Temporary"); - } + if (entry.HasTemporaryValue(property)) + { + builder.Append(" Temporary"); + } - if (entry.IsUnknown(property)) - { - builder.Append(" Unknown"); - } + if (entry.IsUnknown(property)) + { + builder.Append(" Unknown"); + } - if (entry.HasOriginalValuesSnapshot - && property.GetOriginalValueIndex() != -1) - { - var originalValue = entry.GetOriginalValue(property); - if (!Equals(originalValue, currentValue)) + if (entry.HasOriginalValuesSnapshot + && property.GetOriginalValueIndex() != -1) { - builder.Append(" Originally "); - AppendValue(originalValue); + var originalValue = entry.GetOriginalValue(property); + if (!Equals(originalValue, currentValue)) + { + builder.Append(" Originally "); + AppendValue(originalValue); + } } } + + foreach (var complexProperty in structuralType.GetComplexProperties()) + { + builder.AppendLine().Append(tempIndentString); + + builder + .Append(" ") + .Append(complexProperty.Name) + .Append(" (Complex: ") + .Append(complexProperty.ClrType.ShortDisplayName()) + .Append(")"); + + DumpProperties(complexProperty.ComplexType, tempIndent + 2); + } } } else diff --git a/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs new file mode 100644 index 00000000000..6cad13b626a --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingInMemoryTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingInMemoryTest(InMemoryFixture fixture) + : base(fixture) + { + } + + protected override void ExecuteWithStrategyInTransaction( + Action testOperation, + Action nestedTestOperation1 = null, + Action nestedTestOperation2 = null) + { + try + { + base.ExecuteWithStrategyInTransaction(testOperation, nestedTestOperation1, nestedTestOperation2); + } + finally + { + Fixture.Reseed(); + } + } + + protected override async Task ExecuteWithStrategyInTransactionAsync( + Func testOperation, + Func nestedTestOperation1 = null, + Func nestedTestOperation2 = null) + { + try + { + await base.ExecuteWithStrategyInTransactionAsync(testOperation, nestedTestOperation1, nestedTestOperation2); + } + finally + { + Fixture.Reseed(); + } + } + + public class InMemoryFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(w => w.Log(InMemoryEventId.TransactionIgnoredWarning)); + } +} diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs new file mode 100644 index 00000000000..c976b910e0f --- /dev/null +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -0,0 +1,491 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class ComplexTypesTrackingTestBase : IClassFixture + where TFixture : ComplexTypesTrackingTestBase.FixtureBase +{ + protected ComplexTypesTrackingTestBase(TFixture fixture) + { + Fixture = fixture; + } + + protected TFixture Fixture { get; } + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual async Task Can_track_entity_with_complex_objects(EntityState state, bool async) + => await ExecuteWithStrategyInTransactionAsync( + async context => + { + var pub = CreatePub(); + + var entry = state switch + { + EntityState.Unchanged => context.Attach(pub), + EntityState.Deleted => context.Remove(pub), + EntityState.Modified => context.Update(pub), + EntityState.Added => async ? await context.AddAsync(pub) : context.Add(pub), + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + + Assert.Equal(state, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, state == EntityState.Modified); + + if (state == EntityState.Added || state == EntityState.Unchanged) + { + _ = async ? await context.SaveChangesAsync() : context.SaveChanges(); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + } + }); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_type_properties_modified(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePub(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + MarkModified(entry, "EveningActivity.RunnersUp.Members", true); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.RunnersUp.Members")); + + MarkModified(entry, "LunchtimeActivity.Day", true); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + + MarkModified(entry, "EveningActivity.CoverCharge", true); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + + Assert.False(IsModified(entry, "LunchtimeActivity.Name")); + Assert.False(IsModified(entry, "LunchtimeActivity.Description")); + Assert.False(IsModified(entry, "LunchtimeActivity.Notes")); + Assert.False(IsModified(entry, "LunchtimeActivity.CoverCharge")); + Assert.False(IsModified(entry, "LunchtimeActivity.IsTeamBased")); + Assert.False(IsModified(entry, "LunchtimeActivity.Champions.Name")); + Assert.False(IsModified(entry, "LunchtimeActivity.Champions.Members")); + Assert.False(IsModified(entry, "LunchtimeActivity.RunnersUp.Name")); + Assert.False(IsModified(entry, "LunchtimeActivity.RunnersUp.Members")); + Assert.False(IsModified(entry, "EveningActivity.Name")); + Assert.False(IsModified(entry, "EveningActivity.Day")); + Assert.False(IsModified(entry, "EveningActivity.Description")); + Assert.False(IsModified(entry, "EveningActivity.Notes")); + Assert.False(IsModified(entry, "EveningActivity.IsTeamBased")); + Assert.False(IsModified(entry, "EveningActivity.Champions.Name")); + Assert.False(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.False(IsModified(entry, "EveningActivity.RunnersUp.Name")); + Assert.False(IsModified(entry, "FeaturedTeam.Name")); + Assert.False(IsModified(entry, "FeaturedTeam.Members")); + + MarkModified(entry, "EveningActivity.RunnersUp.Members", false); + Assert.Equal(EntityState.Modified, entry.State); + Assert.False(IsModified(entry, "EveningActivity.RunnersUp.Members")); + + MarkModified(entry, "LunchtimeActivity.Day", false); + Assert.Equal(EntityState.Modified, entry.State); + Assert.False(IsModified(entry, "LunchtimeActivity.Day")); + + MarkModified(entry, "EveningActivity.CoverCharge", false); + Assert.Equal(EntityState.Unchanged, entry.State); + Assert.False(IsModified(entry, "EveningActivity.CoverCharge")); + + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_complex_types(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePub(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + WriteCurrentValue(entry, "EveningActivity.Champions.Members", new List { "1", "2", "3" }); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + WriteCurrentValue(entry, "LunchtimeActivity.Day", DayOfWeek.Wednesday); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + WriteCurrentValue(entry, "EveningActivity.CoverCharge", 3.0m); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_complex_types(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePub(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + WriteOriginalValue(entry, "EveningActivity.Champions.Members", new List { "1", "2", "3" }); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + WriteOriginalValue(entry, "LunchtimeActivity.Day", DayOfWeek.Wednesday); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + WriteOriginalValue(entry, "EveningActivity.CoverCharge", 3.0m); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Detect_changes_detects_changes_in_complex_type_properties(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePub(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + pub.EveningActivity.Champions.Members = new List + { + "1", + "2", + "3" + }; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + pub.LunchtimeActivity.Day = DayOfWeek.Wednesday; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + pub.EveningActivity.CoverCharge = 3.0m; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + protected static Pub CreatePub() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() + { + Name = "Banksy" + }, + }, + FeaturedTeam = new() + { + Name = "Not In This Lifetime", + Members = + { + "Slash", + "Axl" + } + } + }; + + protected void AssertPropertyValues(EntityEntry entry) + { + Assert.Equal("The FBI", ReadCurrentValue(entry, "Name")); + Assert.NotNull(ReadCurrentValue(entry, "LunchtimeActivity")); + Assert.Equal("Pub Quiz", ReadCurrentValue(entry, "LunchtimeActivity.Name")); + Assert.Equal(DayOfWeek.Monday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal("A general knowledge pub quiz.", ReadCurrentValue(entry, "LunchtimeActivity.Description")); + Assert.Equal(new[] { "One", "Two", "Three" }, ReadCurrentValue(entry, "LunchtimeActivity.Notes")); + Assert.Equal(2.0m, ReadCurrentValue(entry, "LunchtimeActivity.CoverCharge")); + Assert.True(ReadCurrentValue(entry, "LunchtimeActivity.IsTeamBased")); + Assert.NotNull(ReadCurrentValue(entry, "LunchtimeActivity.Champions")); + Assert.Equal("Clueless", ReadCurrentValue(entry, "LunchtimeActivity.Champions.Name")); + Assert.Equal(new[] { "Boris", "David", "Theresa" }, ReadCurrentValue>(entry, "LunchtimeActivity.Champions.Members")); + Assert.NotNull(ReadCurrentValue(entry, "LunchtimeActivity.RunnersUp")); + Assert.Equal("ZZ", ReadCurrentValue(entry, "LunchtimeActivity.RunnersUp.Name")); + Assert.Equal( + new[] { "Has Beard", "Has Beard", "Is Called Beard" }, + ReadCurrentValue>(entry, "LunchtimeActivity.RunnersUp.Members")); + Assert.NotNull(ReadCurrentValue(entry, "EveningActivity")); + Assert.Equal("Music Quiz", ReadCurrentValue(entry, "EveningActivity.Name")); + Assert.Equal(DayOfWeek.Friday, ReadCurrentValue(entry, "EveningActivity.Day")); + Assert.Equal("A music pub quiz.", ReadCurrentValue(entry, "EveningActivity.Description")); + Assert.Empty(ReadCurrentValue(entry, "EveningActivity.Notes")); + Assert.Equal(5.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.True(ReadCurrentValue(entry, "EveningActivity.IsTeamBased")); + Assert.NotNull(ReadCurrentValue(entry, "EveningActivity.Champions")); + Assert.Equal("Dazed and Confused", ReadCurrentValue(entry, "EveningActivity.Champions.Name")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.NotNull(ReadCurrentValue(entry, "EveningActivity.RunnersUp")); + Assert.Equal("Banksy", ReadCurrentValue(entry, "EveningActivity.RunnersUp.Name")); + Assert.Empty(ReadCurrentValue>(entry, "EveningActivity.RunnersUp.Members")); + Assert.NotNull(ReadCurrentValue(entry, "FeaturedTeam")); + Assert.Equal("Not In This Lifetime", ReadCurrentValue(entry, "FeaturedTeam.Name")); + Assert.Equal(new[] { "Slash", "Axl" }, ReadCurrentValue>(entry, "FeaturedTeam.Members")); + } + + protected void AssertPropertiesModified(EntityEntry entry, bool expected) + { + Assert.Equal(expected, IsModified(entry, "Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Description")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Notes")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.CoverCharge")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.IsTeamBased")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Champions.Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Champions.Members")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.RunnersUp.Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.RunnersUp.Members")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Name")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Day")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Description")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Notes")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.IsTeamBased")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Champions.Name")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.RunnersUp.Name")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.RunnersUp.Members")); + Assert.Equal(expected, IsModified(entry, "FeaturedTeam.Name")); + Assert.Equal(expected, IsModified(entry, "FeaturedTeam.Members")); + } + + protected static TValue ReadCurrentValue(EntityEntry entry, string propertyChain) + => entry.GetInfrastructure().GetCurrentValue(FindProperty(entry, propertyChain)); + + protected static TValue ReadOriginalValue(EntityEntry entry, string propertyChain) + => entry.GetInfrastructure().GetOriginalValue((IProperty)FindProperty(entry, propertyChain)); + + protected static void WriteCurrentValue(EntityEntry entry, string propertyChain, object? value) + => entry.GetInfrastructure().SetProperty(FindProperty(entry, propertyChain), value, isMaterialization: false); + + protected static void WriteOriginalValue(EntityEntry entry, string propertyChain, object? value) + => entry.GetInfrastructure().SetOriginalValue(FindProperty(entry, propertyChain), value); + + protected static bool IsModified(EntityEntry entry, string propertyChain) + => entry.GetInfrastructure().IsModified((IProperty)FindProperty(entry, propertyChain)); + + protected static EntityEntry TrackFromQuery(DbContext context, Pub pub) + => new( + context.GetService().StartTrackingFromQuery( + context.Model.FindEntityType(typeof(Pub))!, pub, new ValueBuffer())); + + protected static void MarkModified(EntityEntry entry, string propertyChain, bool modified) + => entry.GetInfrastructure().SetPropertyModified((IProperty)FindProperty(entry, propertyChain), isModified: modified); + + protected static IPropertyBase FindProperty(EntityEntry entry, string propertyChain) + { + var internalEntry = entry.GetInfrastructure(); + var names = propertyChain.Split("."); + var currentType = (ITypeBase)internalEntry.EntityType; + + IPropertyBase property = null!; + foreach (var name in names) + { + var complexProperty = currentType.FindComplexProperty(name); + if (complexProperty != null) + { + currentType = complexProperty.ComplexType; + property = complexProperty; + } + else + { + property = currentType.FindProperty(name)!; + } + } + + return property; + } + + protected virtual void ExecuteWithStrategyInTransaction( + Action testOperation, + Action? nestedTestOperation1 = null, + Action? nestedTestOperation2 = null) + => TestHelpers.ExecuteWithStrategyInTransaction( + CreateContext, UseTransaction, + testOperation, nestedTestOperation1, nestedTestOperation2); + + protected virtual Task ExecuteWithStrategyInTransactionAsync( + Func testOperation, + Func? nestedTestOperation1 = null, + Func? nestedTestOperation2 = null) + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, UseTransaction, + testOperation, nestedTestOperation1, nestedTestOperation2); + + protected virtual void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + { + } + + protected DbContext CreateContext() + => Fixture.CreateContext(); + + public abstract class FixtureBase : SharedStoreFixtureBase + { + protected override string StoreName + => "ComplexTypesTrackingTest"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + } + } + + protected class Pub + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public Activity LunchtimeActivity { get; set; } = null!; + public Activity EveningActivity { get; set; } = null!; + public Team FeaturedTeam { get; set; } = null!; + // Complex collections: + // public List Activities { get; set; } = null!; + // public List? Teams { get; set; } + } + + protected class Activity + { + public string Name { get; set; } = null!; + public decimal CoverCharge { get; set; } + public bool IsTeamBased { get; set; } + public string? Description { get; set; } + public string[]? Notes { get; set; } + public DayOfWeek Day { get; set; } + public Team Champions { get; set; } = null!; + public Team RunnersUp { get; set; } = null!; + } + + protected class Team + { + public string Name { get; set; } = null!; + public List Members { get; set; } = new(); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs new file mode 100644 index 00000000000..d75f7332f12 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingSqlServerTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingSqlServerTest(SqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class SqlServerFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs new file mode 100644 index 00000000000..980ff315e9a --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingSqliteTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingSqliteTest(SqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class SqliteFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } +} diff --git a/test/EFCore.Tests/DbContextServicesTest.cs b/test/EFCore.Tests/DbContextServicesTest.cs index ea997c27bd7..1cb32bc558d 100644 --- a/test/EFCore.Tests/DbContextServicesTest.cs +++ b/test/EFCore.Tests/DbContextServicesTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; @@ -778,11 +780,34 @@ public void Can_get_replaced_singleton_service_from_scoped_configuration() Assert.IsType(context.GetService()); } + [ComplexType] + private class Tag + { + public string Name { get; set; } + + [Required] + public Stamp Stamp { get; set; } + + public string[] Notes { get; set; } + } + + [ComplexType] + private class Stamp + { + public Guid Code { get; set; } + } + private class Category { public int Id { get; set; } public string Name { get; set; } + [Required] + public Tag Tag { get; set; } + + [Required] + public Stamp Stamp { get; set; } + public List Products { get; set; } } @@ -792,6 +817,12 @@ private class Product public string Name { get; set; } public decimal Price { get; set; } + [Required] + public Tag Tag { get; set; } + + [Required] + public Stamp Stamp { get; set; } + public int CategoryId { get; set; } public Category Category { get; set; } } diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 06cdb176e39..589e4725850 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -51,7 +51,20 @@ public void Local_calls_DetectChanges() changeDetector.DetectChangesCalled = false; var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Big Hedgehogs"; @@ -78,7 +91,20 @@ public void Local_does_not_call_DetectChanges_when_disabled() changeDetector.DetectChangesCalled = false; var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Big Hedgehogs"; @@ -508,7 +534,20 @@ public async Task SaveChanges_calls_DetectChanges_by_default(bool async) Assert.True(context.ChangeTracker.AutoDetectChangesEnabled); var product = (await context.AddAsync( - new Product { Id = 1, Name = "Little Hedgehogs" })).Entity; + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } })).Entity; if (async) { @@ -550,7 +589,20 @@ public async Task Auto_DetectChanges_for_SaveChanges_can_be_switched_off(bool as Assert.False(context.ChangeTracker.AutoDetectChangesEnabled); var product = (await context.AddAsync( - new Product { Id = 1, Name = "Little Hedgehogs" })).Entity; + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } })).Entity; if (async) { @@ -611,8 +663,47 @@ public void DetectChanges_is_called_for_cascade_delete_unless_disabled(bool auto context.ChangeTracker.AutoDetectChangesEnabled = autoDetectChangesEnabled; - var products = new List { new() { Id = 1 }, new() { Id = 2 } }; - var category = context.Attach(new Category { Id = 1, Products = products }).Entity; + var products = new List { new() { Id = 1, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }, new() { Id = 2, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }; + var category = context.Attach(new Category { Id = 1, Products = products, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; Assert.Empty(detectedChangesFor); @@ -638,7 +729,20 @@ public void Entry_calls_DetectChanges_by_default(bool useGenericOverload) { using var context = new ButTheHedgehogContext(InMemoryTestHelpers.Instance.CreateServiceProvider()); var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Cracked Cookies"; @@ -665,7 +769,20 @@ public void Auto_DetectChanges_for_Entry_can_be_switched_off(bool useGenericOver context.ChangeTracker.AutoDetectChangesEnabled = false; var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Cracked Cookies"; @@ -697,65 +814,455 @@ public async Task Add_Attach_Remove_Update_do_not_call_DetectChanges() changeDetector.DetectChangesCalled = false; context.Add( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Add( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AddRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AddRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AddRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.AddRange( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); await context.AddAsync( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddAsync( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddRangeAsync( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddRangeAsync( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddRangeAsync( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); await context.AddRangeAsync( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.Attach( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Attach( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AttachRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AttachRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AttachRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.AttachRange( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.Update( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Update( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.UpdateRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.UpdateRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.UpdateRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.UpdateRange( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.Remove( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Remove( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.RemoveRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.RemoveRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.RemoveRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.RemoveRange( - new List { new Product { Id = id, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); Assert.False(changeDetector.DetectChangesCalled); diff --git a/test/EFCore.Tests/DbContextTrackingTest.cs b/test/EFCore.Tests/DbContextTrackingTest.cs index b73699f94e7..b2424175a3b 100644 --- a/test/EFCore.Tests/DbContextTrackingTest.cs +++ b/test/EFCore.Tests/DbContextTrackingTest.cs @@ -44,22 +44,74 @@ private static async Task TrackEntitiesTest( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principalEntry = await categoryAdder(context, principal); @@ -129,22 +181,74 @@ private static async Task TrackMultipleEntitiesTest( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await adder(context, new object[] { principal, dependent }); @@ -202,12 +306,38 @@ private static async Task TrackEntitiesDefaultValueTest( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var categoryEntry1 = await categoryAdder(context, category1); @@ -346,12 +476,38 @@ private static async Task TrackMultipleEntitiesDefaultValuesTest( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await categoryAdder(context, new[] { category1 }); @@ -439,22 +595,74 @@ private static async Task TrackEntitiesTestNonGeneric( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principalEntry = await categoryAdder(context, principal); @@ -524,22 +732,74 @@ private static async Task TrackMultipleEntitiesTestEnumerable( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await adder(context, new object[] { principal, dependent }); @@ -597,12 +857,38 @@ private static async Task TrackEntitiesDefaultValuesTestNonGeneric( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var categoryEntry1 = await categoryAdder(context, category1); @@ -669,12 +955,38 @@ private static async Task TrackMultipleEntitiesDefaultValueTestEnumerable( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await categoryAdder( @@ -848,7 +1160,20 @@ private async Task ChangeStateWithMethod( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var entity = new Category { Id = 1, Name = "Beverages" }; + var entity = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var entry = context.Entry(entity); entry.State = initialState; @@ -862,13 +1187,39 @@ private async Task ChangeStateWithMethod( public void Can_attach_with_inconsistent_FK_principal_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -895,13 +1246,39 @@ public void Can_attach_with_inconsistent_FK_principal_first_fully_fixed_up() public void Can_attach_with_inconsistent_FK_dependent_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -926,13 +1303,39 @@ public void Can_attach_with_inconsistent_FK_dependent_first_fully_fixed_up() public void Can_attach_with_inconsistent_FK_principal_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -957,13 +1360,39 @@ public void Can_attach_with_inconsistent_FK_principal_first_collection_not_fixed public void Can_attach_with_inconsistent_FK_dependent_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -988,12 +1417,38 @@ public void Can_attach_with_inconsistent_FK_dependent_first_collection_not_fixed public void Can_attach_with_inconsistent_FK_principal_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1018,12 +1473,38 @@ public void Can_attach_with_inconsistent_FK_principal_first_reference_not_fixed_ public void Can_attach_with_inconsistent_FK_dependent_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1048,13 +1529,39 @@ public void Can_attach_with_inconsistent_FK_dependent_first_reference_not_fixed_ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1081,13 +1588,39 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_fully_ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1112,13 +1645,39 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_fully_ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1143,13 +1702,39 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_collec public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1174,12 +1759,38 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_collec public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1204,12 +1815,38 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_refere public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1235,15 +1872,54 @@ public void Can_attach_with_inconsistent_FK_principal_first_fully_fixed_up_with_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1272,16 +1948,55 @@ public void Can_attach_with_inconsistent_FK_dependent_first_fully_fixed_up_with_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1314,14 +2029,40 @@ public void Can_attach_with_inconsistent_FK_principal_first_collection_not_fixed var category7 = context.Attach( new Category { Id = 7, Products = new List() }).Entity; - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1354,14 +2095,40 @@ public void Can_attach_with_inconsistent_FK_dependent_first_collection_not_fixed var category7 = context.Attach( new Category { Id = 7, Products = new List() }).Entity; - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1394,12 +2161,38 @@ public void Can_attach_with_inconsistent_FK_principal_first_reference_not_fixed_ var category7 = context.Attach( new Category { Id = 7, Products = new List() }).Entity; - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1427,14 +2220,53 @@ public void Can_attach_with_inconsistent_FK_dependent_first_reference_not_fixed_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1461,15 +2293,54 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_fully_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1497,16 +2368,55 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_fully_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1537,15 +2447,54 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_collec { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1574,16 +2523,55 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_collec { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1614,14 +2602,53 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_refere { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1649,14 +2676,53 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_refere { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; diff --git a/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs b/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs index a14ff66393d..85f6b37decd 100644 --- a/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs @@ -378,11 +378,11 @@ public virtual void Properties_can_be_made_concurrency_tokens() Assert.False(complexType.FindProperty("Bottom").IsConcurrencyToken); Assert.Equal(-1, complexType.FindProperty(Customer.IdProperty.Name).GetOriginalValueIndex()); - Assert.Equal(2, complexType.FindProperty("Up").GetOriginalValueIndex()); + Assert.Equal(6, complexType.FindProperty("Up").GetOriginalValueIndex()); Assert.Equal(-1, complexType.FindProperty("Down").GetOriginalValueIndex()); - Assert.Equal(0, complexType.FindProperty("Charm").GetOriginalValueIndex()); + Assert.Equal(4, complexType.FindProperty("Charm").GetOriginalValueIndex()); Assert.Equal(-1, complexType.FindProperty("Strange").GetOriginalValueIndex()); - Assert.Equal(1, complexType.FindProperty("Top").GetOriginalValueIndex()); + Assert.Equal(5, complexType.FindProperty("Top").GetOriginalValueIndex()); Assert.Equal(-1, complexType.FindProperty("Bottom").GetOriginalValueIndex()); Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, complexType.GetChangeTrackingStrategy()); From d194501421093871ec2cf26eb0385a4ccda58c6c Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Thu, 10 Aug 2023 11:32:07 +0100 Subject: [PATCH 2/4] Initial change tracking and DetectChanges for complex types Part of #9906 --- .../Storage/Internal/InMemoryTable.cs | 20 +- .../Update/ColumnModification.cs | 59 +- .../Update/ModificationCommand.cs | 6 +- .../ChangeTracking/Internal/ChangeDetector.cs | 2 +- .../EmptyShadowValuesFactoryFactory.cs | 4 +- .../ChangeTracking/Internal/IInternalEntry.cs | 10 +- .../InternalEntityEntry.ComplexEntries.cs | 62 - ...nternalEntityEntry.InternalComplexEntry.cs | 1177 ------------ .../InternalEntityEntry.OriginalValues.cs | 6 +- .../Internal/InternalEntityEntry.cs | 61 +- .../Internal/OriginalValuesFactoryFactory.cs | 4 +- .../RelationshipSnapshotFactoryFactory.cs | 4 +- .../Internal/ShadowValuesFactoryFactory.cs | 4 +- .../Internal/SidecarValuesFactoryFactory.cs | 4 +- .../Internal/SnapshotFactoryFactory.cs | 26 +- .../Internal/SnapshotFactoryFactory`.cs | 6 +- .../ChangeTracking/Internal/StateManager.cs | 2 +- src/EFCore/Infrastructure/ModelValidator.cs | 9 +- src/EFCore/Metadata/IEntityType.cs | 34 + .../Metadata/Internal/ClrAccessorFactory.cs | 2 +- .../Internal/ClrPropertyGetterFactory.cs | 6 +- .../Internal/ClrPropertySetterFactory.cs | 29 +- src/EFCore/Metadata/Internal/ComplexType.cs | 96 - .../Internal/ComplexTypeExtensions.cs | 58 - .../Internal/ConstructorBindingFactory.cs | 8 +- .../Metadata/Internal/EntityTypeExtensions.cs | 42 +- .../Metadata/Internal/IRuntimeComplexType.cs | 8 - .../Metadata/Internal/IRuntimeEntityType.cs | 149 +- .../Metadata/Internal/IRuntimeTypeBase.cs | 120 -- .../Internal/PropertyAccessorsFactory.cs | 2 +- src/EFCore/Metadata/Internal/PropertyBase.cs | 29 +- .../Metadata/Internal/PropertyExtensions.cs | 9 + src/EFCore/Metadata/RuntimeComplexType.cs | 12 - src/EFCore/Metadata/RuntimeEntityType.cs | 128 +- src/EFCore/Metadata/RuntimePropertyBase.cs | 2 +- src/EFCore/Metadata/RuntimeTypeBase.cs | 115 -- src/EFCore/Properties/CoreStrings.Designer.cs | 8 - src/EFCore/Properties/CoreStrings.resx | 3 - src/EFCore/Update/UpdateEntryExtensions.cs | 98 +- .../ComplexTypesTrackingInMemoryTest.cs | 51 + .../ComplexTypesTrackingTestBase.cs | 1632 +++++++++++++++++ .../ComplexTypesTrackingSqlServerTest.cs | 26 + .../ComplexTypesTrackingSqliteTest.cs | 26 + test/EFCore.Tests/DbContextServicesTest.cs | 31 + test/EFCore.Tests/DbContextTest.cs | 583 +++++- test/EFCore.Tests/DbContextTrackingTest.cs | 1248 ++++++++++++- .../ModelBuilding/ComplexTypeTestBase.cs | 86 +- test/EFCore.Tests/ModelBuilding/TestModel.cs | 6 +- 48 files changed, 4061 insertions(+), 2052 deletions(-) delete mode 100644 src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs delete mode 100644 src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs create mode 100644 test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs index 31f421cdf30..2be95605cab 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTable.cs @@ -5,6 +5,7 @@ using System.Globalization; using Microsoft.EntityFrameworkCore.InMemory.Internal; using Microsoft.EntityFrameworkCore.InMemory.ValueGeneration.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace Microsoft.EntityFrameworkCore.InMemory.Storage.Internal; @@ -44,7 +45,7 @@ public InMemoryTable( _sensitiveLoggingEnabled = sensitiveLoggingEnabled; _nullabilityCheckEnabled = nullabilityCheckEnabled; _rows = new Dictionary(_keyValueFactory.EqualityComparer); - var properties = entityType.GetProperties().ToList(); + var properties = entityType.GetFlattenedProperties().ToList(); _propertyCount = properties.Count; foreach (var property in properties) @@ -163,7 +164,7 @@ private static List GetKeyComparers(IEnumerable proper /// public virtual void Create(IUpdateEntry entry, IDiagnosticsLogger updateLogger) { - var properties = entry.EntityType.GetProperties().ToList(); + var properties = entry.EntityType.GetFlattenedProperties().ToList(); var row = new object?[properties.Count]; var nullabilityErrors = new List(); @@ -171,7 +172,7 @@ public virtual void Create(IUpdateEntry entry, IDiagnosticsLogger(); for (var index = 0; index < properties.Count; index++) { - IsConcurrencyConflict(entry, properties[index], row[index], concurrencyConflicts); + IsConcurrencyConflict(entry, properties[index], row[properties[index].GetIndex()], concurrencyConflicts); } if (concurrencyConflicts.Count > 0) @@ -266,7 +267,7 @@ public virtual void Update(IUpdateEntry entry, IDiagnosticsLogger(); @@ -274,19 +275,20 @@ public virtual void Update(IUpdateEntry entry, IDiagnosticsLogger 0) diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index 62698cfeaae..a59598a9480 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -178,7 +178,6 @@ 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 @@ -186,7 +185,7 @@ public virtual object? Value /// 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); + => entry.GetOriginalValue(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -195,10 +194,10 @@ public virtual object? Value /// 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); + => entry.GetOriginalProviderValue(property); private void SetOriginalValue(object? value) - => GetEntry((IInternalEntry)Entry!, Property!).SetOriginalValue(Property!, value); + => Entry!.SetOriginalValue(Property!, value); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -207,7 +206,7 @@ private void SetOriginalValue(object? value) /// 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); + => entry.GetCurrentValue(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -216,7 +215,7 @@ private void SetOriginalValue(object? value) /// 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); + => entry.GetCurrentProviderValue(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -225,7 +224,7 @@ private void SetOriginalValue(object? value) /// 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); + => entry.SetStoreGeneratedValue(property, value); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -234,7 +233,7 @@ public static void SetStoreGeneratedValue(IUpdateEntry entry, IProperty property /// 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); + => entry.IsModified(property); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -243,19 +242,7 @@ public static bool IsModified(IUpdateEntry entry, IProperty property) /// 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. + => entry.IsStoreGenerated(property); /// public virtual string? JsonPath { get; } @@ -275,30 +262,28 @@ public virtual void AddSharedColumnModification(IColumnModification modification 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); + var existingEntry = Entry; + var newEntry = modification.Entry; if (_sensitiveLoggingEnabled) { throw new InvalidOperationException( RelationalStrings.ConflictingRowValuesSensitive( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), Entry.BuildCurrentValuesString(Entry.EntityType.FindPrimaryKey()!.Properties), - GetEntry((IInternalEntry)Entry!, Property).BuildCurrentValuesString(new[] { Property }), + Entry.BuildCurrentValuesString(new[] { Property }), newEntry.BuildCurrentValuesString(new[] { modification.Property }), ColumnName)); } throw new InvalidOperationException( RelationalStrings.ConflictingRowValues( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), new[] { Property }.Format(), new[] { modification.Property }.Format(), ColumnName)); -#pragma warning restore EF1001 // Internal EF Core API usage. } if (UseOriginalValueParameter) @@ -331,15 +316,14 @@ 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); + var existingEntry = Entry; + var newEntry = modification.Entry; if (_sensitiveLoggingEnabled) { throw new InvalidOperationException( RelationalStrings.ConflictingOriginalRowValuesSensitive( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), Entry.BuildCurrentValuesString(Entry.EntityType.FindPrimaryKey()!.Properties), existingEntry.BuildOriginalValuesString(new[] { Property }), newEntry.BuildOriginalValuesString(new[] { modification.Property }), @@ -348,12 +332,11 @@ public virtual void AddSharedColumnModification(IColumnModification modification throw new InvalidOperationException( RelationalStrings.ConflictingOriginalRowValues( - existingEntry.StructuralType.DisplayName(), - newEntry.StructuralType.DisplayName(), + existingEntry.EntityType.DisplayName(), + newEntry.EntityType.DisplayName(), new[] { Property }.Format(), new[] { modification.Property }.Format(), ColumnName)); -#pragma warning restore EF1001 // Internal EF Core API usage. } } diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index e154e96d609..1e2703cc417 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -604,7 +604,7 @@ void HandleSharedColumns( result.Path.Insert(0, pathEntry); } - var modifiedMembers = entry.EntityType.GetProperties().Where(entry.IsModified).ToList(); + var modifiedMembers = entry.EntityType.GetFlattenedProperties().Where(entry.IsModified).ToList(); if (modifiedMembers.Count == 1) { result.Property = modifiedMembers[0]; @@ -854,7 +854,7 @@ private void WriteJson( #pragma warning restore EF1001 // Internal EF Core API usage. writer.WriteStartObject(); - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.IsKey()) { @@ -1108,7 +1108,7 @@ public void RecordValue(IColumnMapping mapping, IUpdateEntry entry) { _currentValue = Update.ColumnModification.GetCurrentProviderValue(entry, property); } - + _write = !_originalValueInitialized || !mapping.Column.ProviderValueComparer.Equals(_originalValue, _currentValue); diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 79f7abdbf8d..1f1c141aa93 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -226,7 +226,7 @@ private bool LocalDetectChanges(InternalEntityEntry entry) OnDetectingEntityChanges(entry); - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.GetOriginalValueIndex() >= 0 && !entry.IsModified(property) diff --git a/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs index d5b61954ce0..b30f0f8ba22 100644 --- a/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/EmptyShadowValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.ShadowPropertyCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.ShadowPropertyCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs index 751a6ac5257..bd973bb81df 100644 --- a/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/IInternalEntry.cs @@ -27,7 +27,7 @@ public interface IInternalEntry /// any release. You should only use it directly in 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; } + IRuntimeEntityType EntityType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -296,14 +296,6 @@ void SetEntityState( 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 diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs deleted file mode 100644 index 84035910e55..00000000000 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.ComplexEntries.cs +++ /dev/null @@ -1,62 +0,0 @@ -// 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/InternalEntityEntry.InternalComplexEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs deleted file mode 100644 index a714812a4e6..00000000000 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.InternalComplexEntry.cs +++ /dev/null @@ -1,1177 +0,0 @@ -// 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.OriginalValues.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.OriginalValues.cs index 3b4f2915c05..fd3b1581ea5 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.OriginalValues.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.OriginalValues.cs @@ -13,7 +13,7 @@ private readonly struct OriginalValues public OriginalValues(IInternalEntry entry) { - _values = entry.StructuralType.OriginalValuesFactory(entry); + _values = entry.EntityType.OriginalValuesFactory(entry); } public object? GetValue(IInternalEntry entry, IProperty property) @@ -72,7 +72,7 @@ public void RejectChanges(IInternalEntry entry) return; } - foreach (var property in entry.StructuralType.GetProperties()) + foreach (var property in entry.EntityType.GetFlattenedProperties()) { var index = property.GetOriginalValueIndex(); if (index >= 0) @@ -89,7 +89,7 @@ public void AcceptChanges(IInternalEntry entry) return; } - foreach (var property in entry.StructuralType.GetProperties()) + foreach (var property in entry.EntityType.GetFlattenedProperties()) { var index = property.GetOriginalValueIndex(); if (index >= 0) diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index 2b16492093b..f7c233678a3 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -24,7 +24,6 @@ public sealed partial class InternalEntityEntry : IUpdateEntry, IInternalEntry 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 @@ -42,9 +41,8 @@ public InternalEntityEntry( Entity = entity; _shadowValues = EntityType.EmptyShadowValuesFactory(); _stateData = new StateData(EntityType.PropertyCount, EntityType.NavigationCount); - _complexEntries = new ComplexEntries(this); - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.IsShadowProperty()) { @@ -70,8 +68,6 @@ 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); } /// @@ -296,7 +292,7 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc && newState != EntityState.Detached) { // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.IsKey() && HasTemporaryValue(property)) { @@ -316,18 +312,13 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.Modified, flagged: true); // Hot path; do not use LINQ - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { 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) @@ -340,11 +331,6 @@ 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) @@ -705,8 +691,7 @@ public void OnComplexPropertyModified(IComplexProperty property, bool isModified } else if (currentState == EntityState.Modified && !isModified - && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified) - && _complexEntries.All(e => e.EntityState == EntityState.Unchanged)) + && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified)) { _stateData.EntityState = EntityState.Unchanged; } @@ -1238,15 +1223,6 @@ 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 @@ -1469,11 +1445,6 @@ private void SetProperty( SetIsLoaded(navigation, value != null); } - if (propertyBase is IComplexProperty complexProperty) - { - _complexEntries.SetValue(value, this, complexProperty); - } - StateManager.InternalEntityEntryNotifier.PropertyChanged(this, propertyBase, setModified); } } @@ -1526,7 +1497,7 @@ public void AcceptChanges() { if (!_storeGeneratedValues.IsEmpty) { - foreach (var property in EntityType.GetProperties()) + foreach (var property in EntityType.GetFlattenedProperties()) { var storeGeneratedIndex = property.GetStoreGeneratedIndex(); if (storeGeneratedIndex != -1 @@ -1545,11 +1516,6 @@ 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) { @@ -1581,7 +1547,7 @@ public InternalEntityEntry PrepareToSave() if (EntityState == EntityState.Added) { - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.GetBeforeSaveBehavior() == PropertySaveBehavior.Throw && !HasTemporaryValue(property) @@ -1609,7 +1575,7 @@ public InternalEntityEntry PrepareToSave() } else if (EntityState == EntityState.Modified) { - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Throw && IsModified(property)) @@ -1625,7 +1591,7 @@ public InternalEntityEntry PrepareToSave() } else if (EntityState == EntityState.Deleted) { - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { CheckForUnknownKey(property); } @@ -1731,7 +1697,7 @@ public void HandleConceptualNulls(bool sensitiveLoggingEnabled, bool force, bool } else { - var property = EntityType.GetProperties().FirstOrDefault( + var property = EntityType.GetFlattenedProperties().FirstOrDefault( p => (EntityState != EntityState.Modified || IsModified(p)) && _stateData.IsPropertyFlagged(p.GetIndex(), PropertyFlag.Null)); @@ -1768,11 +1734,6 @@ public void DiscardStoreGeneratedValues() _storeGeneratedValues = new SidecarValues(); _stateData.FlagAllProperties(EntityType.PropertyCount, PropertyFlag.IsStoreGenerated, false); } - - foreach (var complexEntry in _complexEntries) - { - complexEntry.DiscardStoreGeneratedValues(); - } } /// @@ -1931,7 +1892,7 @@ private static IEnumerable GetNotificationProperties( { if (string.IsNullOrEmpty(propertyName)) { - foreach (var property in entityType.GetProperties() + foreach (var property in entityType.GetFlattenedProperties() .Where(p => p.GetAfterSaveBehavior() == PropertySaveBehavior.Save)) { yield return property; @@ -2099,7 +2060,7 @@ public DebugView DebugView IEntityType IUpdateEntry.EntityType => EntityType; - IRuntimeTypeBase IInternalEntry.StructuralType + IRuntimeEntityType IInternalEntry.EntityType => EntityType; object IInternalEntry.Object diff --git a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs index f0a15b610de..ebc0663ccdc 100644 --- a/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/OriginalValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.OriginalValueCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.OriginalValueCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs index 8c4bb0e69f8..548aceb5267 100644 --- a/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/RelationshipSnapshotFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.RelationshipPropertyCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.RelationshipPropertyCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs index 02a03f14ae4..579768ad387 100644 --- a/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/ShadowValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.ShadowPropertyCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.ShadowPropertyCount; /// /// 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 d43f8eba4b1..e0f758c815e 100644 --- a/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SidecarValuesFactoryFactory.cs @@ -28,8 +28,8 @@ protected override int GetPropertyIndex(IPropertyBase propertyBase) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override int GetPropertyCount(IRuntimeTypeBase typeBase) - => typeBase.StoreGeneratedCount; + protected override int GetPropertyCount(IRuntimeEntityType entityType) + => entityType.StoreGeneratedCount; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index 49a7381720f..afb30d4717c 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -21,11 +21,11 @@ public abstract class SnapshotFactoryFactory /// any release. You should only use it directly in 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 CreateEmpty(IRuntimeTypeBase typeBase) - => GetPropertyCount(typeBase) == 0 + public virtual Func CreateEmpty(IRuntimeEntityType entityType) + => GetPropertyCount(entityType) == 0 ? (() => Snapshot.Empty) : Expression.Lambda>( - CreateConstructorExpression(typeBase, null!)) + CreateConstructorExpression(entityType, null!)) .Compile(); /// @@ -35,15 +35,15 @@ public virtual Func CreateEmpty(IRuntimeTypeBase typeBase) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected virtual Expression CreateConstructorExpression( - IRuntimeTypeBase typeBase, + IRuntimeEntityType entityType, ParameterExpression? parameter) { - var count = GetPropertyCount(typeBase); + var count = GetPropertyCount(entityType); var types = new Type[count]; var propertyBases = new IPropertyBase?[count]; - foreach (var propertyBase in typeBase.GetSnapshottableMembers()) + foreach (var propertyBase in entityType.GetSnapshottableMembers()) { var index = GetPropertyIndex(propertyBase); if (index >= 0) @@ -62,7 +62,7 @@ protected virtual Expression CreateConstructorExpression( { snapshotExpressions.Add( CreateSnapshotExpression( - typeBase.ClrType, + entityType.ClrType, parameter, types.Skip(i).Take(Snapshot.MaxGenericTypes).ToArray(), propertyBases.Skip(i).Take(Snapshot.MaxGenericTypes).ToList())); @@ -77,7 +77,7 @@ protected virtual Expression CreateConstructorExpression( } else { - constructorExpression = CreateSnapshotExpression(typeBase.ClrType, parameter, types, propertyBases); + constructorExpression = CreateSnapshotExpression(entityType.ClrType, parameter, types, propertyBases); } return constructorExpression; @@ -119,6 +119,12 @@ protected virtual Expression CreateSnapshotExpression( continue; } + if (propertyBase is IComplexProperty complexProperty) + { + arguments[i] = CreateSnapshotValueExpression(CreateReadValueExpression(parameter, complexProperty), complexProperty); + continue; + } + if (propertyBase.IsShadowProperty()) { arguments[i] = CreateSnapshotValueExpression(CreateReadShadowValueExpression(parameter, propertyBase), propertyBase); @@ -243,7 +249,7 @@ protected virtual Expression CreateReadValueExpression( => Expression.Call( parameter, InternalEntityEntry.MakeGetCurrentValueMethod(property.ClrType), - Expression.Constant(property, typeof(IProperty))); + Expression.Constant(property, typeof(IPropertyBase))); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -259,7 +265,7 @@ protected virtual Expression CreateReadValueExpression( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected abstract int GetPropertyCount(IRuntimeTypeBase typeBase); + protected abstract int GetPropertyCount(IRuntimeEntityType entityType); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs index 66f8fc1c536..08328f92b27 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory`.cs @@ -19,9 +19,9 @@ public abstract class SnapshotFactoryFactory : SnapshotFactoryFactory /// any release. You should only use it directly in 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 Create(IRuntimeTypeBase typeBase) + public virtual Func Create(IRuntimeEntityType entityType) { - if (GetPropertyCount(typeBase) == 0) + if (GetPropertyCount(entityType) == 0) { return _ => Snapshot.Empty; } @@ -29,7 +29,7 @@ public virtual Func Create(IRuntimeTypeBase typeBase) var parameter = Expression.Parameter(typeof(TInput), "source"); return Expression.Lambda>( - CreateConstructorExpression(typeBase, parameter), + CreateConstructorExpression(entityType, parameter), parameter) .Compile(); } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 5913317de23..8982cf5f077 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -278,7 +278,7 @@ public virtual InternalEntityEntry CreateEntry(IDictionary valu var runtimeEntityType = (IRuntimeEntityType)entityType; var valuesArray = new object?[runtimeEntityType.PropertyCount]; var shadowPropertyValuesArray = new object?[runtimeEntityType.ShadowPropertyCount]; - foreach (var property in entityType.GetProperties()) + foreach (var property in entityType.GetFlattenedProperties()) { valuesArray[i++] = values.TryGetValue(property.Name, out var value) ? value diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 66e659b6a82..76eeb462189 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -181,14 +181,7 @@ void Validate(IConventionTypeBase typeBase) CoreStrings.ComplexPropertyOptional(typeBase.DisplayName(), complexProperty.Name)); } - if (complexProperty.ComplexType.ClrType.IsValueType) - { - throw new InvalidOperationException( - CoreStrings.ValueComplexType( - typeBase.DisplayName(), complexProperty.Name, complexProperty.ComplexType.ClrType.ShortDisplayName())); - } - - if (complexProperty.ComplexType.GetMembers().Count() == 0) + if (!complexProperty.ComplexType.GetMembers().Any()) { throw new InvalidOperationException( CoreStrings.EmptyComplexType(complexProperty.ComplexType.DisplayName())); diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 0d334942a20..6a02f319336 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -486,6 +486,40 @@ public interface IEntityType : IReadOnlyEntityType, ITypeBase /// new IEnumerable GetDeclaredTriggers(); + /// + /// Returns all properties, including those on complex types. + /// + /// The properties. + IEnumerable GetFlattenedProperties() + { + foreach (var property in GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(GetComplexProperties())) + { + yield return property; + } + + IEnumerable ReturnComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(complexType.GetComplexProperties())) + { + yield return property; + } + } + } + } + internal const DynamicallyAccessedMemberTypes DynamicallyAccessedMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors diff --git a/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs b/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs index 246bebc6539..3d3d7c8e790 100644 --- a/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs @@ -44,7 +44,7 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert { var boundMethod = propertyBase != null ? GenericCreate.MakeGenericMethod( - propertyBase.DeclaringType.ClrType, + propertyBase.DeclaringType.ContainingEntityType.ClrType, propertyBase.ClrType, propertyBase.ClrType.UnwrapNullableType()) : GenericCreate.MakeGenericMethod( diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs index b1c1e83f580..8e0eec325f4 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs @@ -32,10 +32,12 @@ protected override IClrPropertyGetter CreateGeneric(setter) : new ClrPropertySetter(setter); - Expression CreateMemberAssignment(Expression parameter) - => propertyBase?.IsIndexerProperty() == true + Expression CreateMemberAssignment(IPropertyBase? property, Expression typeParameter) + { + var targetStructuralType = typeParameter; + if (property?.DeclaringType is IComplexType complexType) + { + targetStructuralType = PropertyBase.CreateMemberAccess( + complexType.ComplexProperty, + typeParameter, + complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false)); + } + + return propertyBase?.IsIndexerProperty() == true ? Expression.Assign( Expression.MakeIndex( - entityParameter, (PropertyInfo)memberInfo, new List { Expression.Constant(propertyBase.Name) }), + targetStructuralType, (PropertyInfo)memberInfo, new List { Expression.Constant(propertyBase.Name) }), convertedParameter) - : Expression.MakeMemberAccess(parameter, memberInfo).Assign(convertedParameter); + : Expression.MakeMemberAccess(targetStructuralType, memberInfo).Assign(convertedParameter); + } } } diff --git a/src/EFCore/Metadata/Internal/ComplexType.cs b/src/EFCore/Metadata/Internal/ComplexType.cs index 6c22afea45b..bdd17fc02d1 100644 --- a/src/EFCore/Metadata/Internal/ComplexType.cs +++ b/src/EFCore/Metadata/Internal/ComplexType.cs @@ -22,17 +22,10 @@ public class ComplexType : TypeBase, IMutableComplexType, IConventionComplexType private ConfigurationSource? _serviceOnlyConstructorBindingConfigurationSource; // Warning: Never access these fields directly as access needs to be thread-safe - private PropertyCounts? _counts; - // _serviceOnlyConstructorBinding needs to be set as well whenever _constructorBinding is set private InstantiationBinding? _constructorBinding; private InstantiationBinding? _serviceOnlyConstructorBinding; - private Func? _originalValuesFactory; - private Func? _temporaryValuesFactory; - private Func? _storeGeneratedValuesFactory; - private Func? _shadowValuesFactory; - private Func? _emptyShadowValuesFactory; private IProperty[]? _foreignKeyProperties; private IProperty[]? _valueGeneratingProperties; @@ -362,95 +355,6 @@ public override IEnumerable FindMembersInHierarchy(string name) => FindPropertiesInHierarchy(name) .Concat(FindComplexPropertiesInHierarchy(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 virtual PropertyCounts Counts - => NonCapturingLazyInitializer.EnsureInitialized( - ref _counts, this, static complexType => - { - complexType.EnsureReadOnly(); - return complexType.CalculateCounts(); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 - => NonCapturingLazyInitializer.EnsureInitialized( - ref _originalValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new OriginalValuesFactoryFactory().Create(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 StoreGeneratedValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _storeGeneratedValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new StoreGeneratedValuesFactoryFactory().CreateEmpty(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 - => NonCapturingLazyInitializer.EnsureInitialized( - ref _temporaryValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new TemporaryValuesFactoryFactory().Create(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 ShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _shadowValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new ShadowValuesFactoryFactory().Create(complexType); - }); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 EmptyShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _emptyShadowValuesFactory, this, - static complexType => - { - complexType.EnsureReadOnly(); - return new EmptyShadowValuesFactoryFactory().CreateEmpty(complexType); - }); - /// /// This is an internal API that supports the 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/ComplexTypeExtensions.cs b/src/EFCore/Metadata/Internal/ComplexTypeExtensions.cs index 54d85423508..302ed0e455b 100644 --- a/src/EFCore/Metadata/Internal/ComplexTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/ComplexTypeExtensions.cs @@ -26,62 +26,4 @@ public static bool UseEagerSnapshots(this IReadOnlyComplexType complexType) return changeTrackingStrategy == ChangeTrackingStrategy.Snapshot || changeTrackingStrategy == ChangeTrackingStrategy.ChangedNotifications; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 PropertyCounts CalculateCounts(this IRuntimeComplexType complexType) - { - var propertyIndex = 0; - var complexPropertyIndex = 0; - var originalValueIndex = 0; - var shadowIndex = 0; - var storeGenerationIndex = 0; - var relationshipIndex = ((IRuntimeTypeBase)complexType.ComplexProperty.DeclaringType).Counts.RelationshipCount; - - var baseCounts = (complexType as ComplexType)?.BaseType?.Counts; - if (baseCounts != null) - { - propertyIndex = baseCounts.PropertyCount; - originalValueIndex = baseCounts.OriginalValueCount; - shadowIndex = baseCounts.ShadowCount; - storeGenerationIndex = baseCounts.StoreGeneratedCount; - } - - foreach (var property in complexType.GetProperties()) - { - var indexes = new PropertyIndexes( - index: propertyIndex++, - originalValueIndex: property.RequiresOriginalValue() ? originalValueIndex++ : -1, - shadowIndex: property.IsShadowProperty() ? shadowIndex++ : -1, - relationshipIndex: property.IsKey() || property.IsForeignKey() ? relationshipIndex++ : -1, - storeGenerationIndex: property.MayBeStoreGenerated() ? storeGenerationIndex++ : -1); - - ((IRuntimePropertyBase)property).PropertyIndexes = indexes; - } - - foreach (var complexProperty in complexType.GetComplexProperties()) - { - var indexes = new PropertyIndexes( - index: complexPropertyIndex++, - originalValueIndex: -1, - shadowIndex: complexProperty.IsShadowProperty() ? shadowIndex++ : -1, - relationshipIndex: -1, - storeGenerationIndex: -1); - - ((IRuntimePropertyBase)complexProperty).PropertyIndexes = indexes; - } - - return new PropertyCounts( - propertyIndex, - navigationCount: 0, - complexPropertyIndex, - originalValueIndex, - shadowIndex, - relationshipCount: 0, - storeGenerationIndex); - } } diff --git a/src/EFCore/Metadata/Internal/ConstructorBindingFactory.cs b/src/EFCore/Metadata/Internal/ConstructorBindingFactory.cs index 0b4fb379843..64e9f57e024 100644 --- a/src/EFCore/Metadata/Internal/ConstructorBindingFactory.cs +++ b/src/EFCore/Metadata/Internal/ConstructorBindingFactory.cs @@ -115,8 +115,8 @@ private void GetBindings( var foundServiceOnlyBindings = new List(); var bindingFailures = new List>(); - var constructors = type.ClrType.GetTypeInfo() - .DeclaredConstructors.Where(c => !c.IsStatic).ToList(); + var clrType = type.ClrType.UnwrapNullableType(); + var constructors = clrType.GetTypeInfo().DeclaredConstructors.Where(c => !c.IsStatic).ToList(); foreach (var constructor in constructors) { // Trying to find the constructor with the most service properties @@ -172,12 +172,12 @@ private void GetBindings( if (foundBindings.Count == 0 && constructors.Count == 0 - && type.ClrType.IsValueType) + && clrType.IsValueType) { foundBindings.Add(new FactoryMethodBinding( _createInstance, new ParameterBinding[0], - type.ClrType)); + clrType)); } if (foundBindings.Count == 0) diff --git a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs index 841abb56138..7515d9a720a 100644 --- a/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs +++ b/src/EFCore/Metadata/Internal/EntityTypeExtensions.cs @@ -191,17 +191,7 @@ public static PropertyCounts CalculateCounts(this IRuntimeEntityType entityType) ((IRuntimePropertyBase)property).PropertyIndexes = indexes; } - foreach (var complexProperty in entityType.GetDeclaredComplexProperties()) - { - var indexes = new PropertyIndexes( - index: complexPropertyIndex++, - originalValueIndex: -1, - shadowIndex: complexProperty.IsShadowProperty() ? shadowIndex++ : -1, - relationshipIndex: -1, - storeGenerationIndex: -1); - - ((IRuntimePropertyBase)complexProperty).PropertyIndexes = indexes; - } + CountComplexProperties(entityType.GetDeclaredComplexProperties()); var isNotifying = entityType.GetChangeTrackingStrategy() != ChangeTrackingStrategy.Snapshot; @@ -238,6 +228,36 @@ public static PropertyCounts CalculateCounts(this IRuntimeEntityType entityType) shadowIndex, relationshipIndex, storeGenerationIndex); + + void CountComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + var indexes = new PropertyIndexes( + index: complexPropertyIndex++, + originalValueIndex: -1, + shadowIndex: complexProperty.IsShadowProperty() ? shadowIndex++ : -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + + ((IRuntimePropertyBase)complexProperty).PropertyIndexes = indexes; + + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + var complexIndexes = new PropertyIndexes( + index: propertyIndex++, + originalValueIndex: property.RequiresOriginalValue() ? originalValueIndex++ : -1, + shadowIndex: property.IsShadowProperty() ? shadowIndex++ : -1, + relationshipIndex: property.IsKey() || property.IsForeignKey() ? relationshipIndex++ : -1, + storeGenerationIndex: property.MayBeStoreGenerated() ? storeGenerationIndex++ : -1); + + ((IRuntimePropertyBase)property).PropertyIndexes = complexIndexes; + } + + CountComplexProperties(complexType.GetComplexProperties()); + } + } } /// diff --git a/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs b/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs index 272627b3619..a601420ab85 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeComplexType.cs @@ -11,12 +11,4 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// public interface IRuntimeComplexType : IComplexType, IRuntimeTypeBase { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IEnumerable IRuntimeTypeBase.GetSnapshottableMembers() - => GetProperties(); } diff --git a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs index 1bfb9b0d5e8..c8d93b191a1 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeEntityType.cs @@ -34,6 +34,151 @@ public interface IRuntimeEntityType : IEntityType, IRuntimeTypeBase /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IEnumerable IRuntimeTypeBase.GetSnapshottableMembers() - => GetProperties().Concat(GetNavigations()); + IEnumerable GetSnapshottableMembers() + { + foreach (var property in GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(GetComplexProperties())) + { + yield return property; + } + + IEnumerable ReturnComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + yield return complexProperty; + + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(complexType.GetComplexProperties())) + { + yield return property; + } + } + } + + foreach (var navigation in GetNavigations()) + { + yield return navigation; + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + PropertyCounts Counts { 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. + /// + int OriginalValueCount + => Counts.OriginalValueCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 PropertyCount + => Counts.PropertyCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 ShadowPropertyCount + => Counts.ShadowCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 StoreGeneratedCount + => Counts.StoreGeneratedCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 RelationshipPropertyCount + => Counts.RelationshipCount; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 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 + /// any release. You should only use it directly in 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; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 StoreGeneratedValuesFactory { 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. + /// + Func TemporaryValuesFactory { 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. + /// + Func ShadowValuesFactory { 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. + /// + Func EmptyShadowValuesFactory { get; } } diff --git a/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs b/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs index 43fcd3992c6..35dd52bffcf 100644 --- a/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs +++ b/src/EFCore/Metadata/Internal/IRuntimeTypeBase.cs @@ -13,117 +13,6 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// public interface IRuntimeTypeBase : ITypeBase { - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 StoreGeneratedValuesFactory { 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. - /// - Func TemporaryValuesFactory { 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. - /// - Func ShadowValuesFactory { 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. - /// - Func EmptyShadowValuesFactory { 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. - /// - PropertyCounts Counts { 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. - /// - int OriginalValueCount - => Counts.OriginalValueCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 PropertyCount - => Counts.PropertyCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 ShadowPropertyCount - => Counts.ShadowCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 StoreGeneratedCount - => Counts.StoreGeneratedCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 RelationshipPropertyCount - => Counts.RelationshipCount; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 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 @@ -139,13 +28,4 @@ int ComplexPropertyCount /// doing so can result in application failures when updating to a new Entity Framework Core release. /// ConfigurationSource? GetServiceOnlyConstructorBindingConfigurationSource(); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - IEnumerable GetSnapshottableMembers() - => throw new NotImplementedException(); } diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index 4152e5ed1ff..c80aaf67e52 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -46,7 +46,7 @@ private static Func CreateCurrentValueGetter { property.EnsureReadOnly(); - var _ = ((IRuntimeTypeBase)property.DeclaringType).Counts; + _ = ((IRuntimeEntityType)(((IRuntimeTypeBase)property.DeclaringType).ContainingEntityType)).Counts; }); set => NonCapturingLazyInitializer.EnsureInitialized(ref _indexes, value); @@ -445,6 +445,33 @@ public static Expression CreateMemberAccess( return expression; } + if (property?.DeclaringType is IComplexType complexType) + { + instanceExpression = CreateMemberAccess( + complexType.ComplexProperty, + instanceExpression, + complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false)); + + if (!instanceExpression.Type.IsValueType + || instanceExpression.Type.IsNullableValueType()) + { + var instanceVariable = Expression.Variable(instanceExpression.Type, "instance"); + instanceExpression = Expression.Block( + new[] { instanceVariable }, + Expression.Assign(instanceVariable, instanceExpression), + Expression.Condition( + Expression.Equal(instanceVariable, Expression.Constant(null)), + Expression.Default(memberInfo.GetMemberType()), + Expression.MakeMemberAccess(instanceVariable, memberInfo))); + + } else + { + instanceExpression = Expression.MakeMemberAccess(instanceExpression, memberInfo); + } + + return instanceExpression; + } + return Expression.MakeMemberAccess(instanceExpression, memberInfo); } diff --git a/src/EFCore/Metadata/Internal/PropertyExtensions.cs b/src/EFCore/Metadata/Internal/PropertyExtensions.cs index 325ecb4584b..2bbfb3bcceb 100644 --- a/src/EFCore/Metadata/Internal/PropertyExtensions.cs +++ b/src/EFCore/Metadata/Internal/PropertyExtensions.cs @@ -164,4 +164,13 @@ public static bool RequiresOriginalValue(this IReadOnlyProperty property) || property.IsKey() || property.IsForeignKey() || property.IsUniqueIndex(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 RequiresOriginalValue(this IReadOnlyComplexProperty property) + => property.ComplexType.ContainingEntityType.GetChangeTrackingStrategy() != ChangeTrackingStrategy.ChangingAndChangedNotifications; } diff --git a/src/EFCore/Metadata/RuntimeComplexType.cs b/src/EFCore/Metadata/RuntimeComplexType.cs index abc1633db47..7dee1893ddd 100644 --- a/src/EFCore/Metadata/RuntimeComplexType.cs +++ b/src/EFCore/Metadata/RuntimeComplexType.cs @@ -18,9 +18,6 @@ public class RuntimeComplexType : RuntimeTypeBase, IRuntimeComplexType private InstantiationBinding? _constructorBinding; private InstantiationBinding? _serviceOnlyConstructorBinding; - // Warning: Never access these fields directly as access needs to be thread-safe - private PropertyCounts? _counts; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -138,15 +135,6 @@ public virtual InstantiationBinding? ServiceOnlyConstructorBinding set => _serviceOnlyConstructorBinding = 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. - /// - protected override PropertyCounts Counts - => NonCapturingLazyInitializer.EnsureInitialized(ref _counts, this, static complexType => complexType.CalculateCounts()); - /// /// Returns a string that represents the current object. /// diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 1dd442c4aa9..b409c61baa0 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -50,10 +50,14 @@ private readonly SortedDictionary _triggers // Warning: Never access these fields directly as access needs to be thread-safe private PropertyCounts? _counts; - private Func? _relationshipSnapshotFactory; private IProperty[]? _foreignKeyProperties; private IProperty[]? _valueGeneratingProperties; + private Func? _originalValuesFactory; + private Func? _temporaryValuesFactory; + private Func? _storeGeneratedValuesFactory; + private Func? _shadowValuesFactory; + private Func? _emptyShadowValuesFactory; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -791,7 +795,7 @@ public virtual InstantiationBinding? ServiceOnlyConstructorBinding /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override PropertyCounts Counts + public virtual PropertyCounts Counts => NonCapturingLazyInitializer.EnsureInitialized(ref _counts, this, static entityType => entityType.CalculateCounts()); /// @@ -1246,4 +1250,124 @@ IEnumerable IEntityType.GetServiceProperties() PropertyAccessMode IReadOnlyEntityType.GetNavigationAccessMode() => throw new InvalidOperationException(CoreStrings.RuntimeModelMissingData); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetOriginalValuesFactory(Func factory) + => _originalValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetStoreGeneratedValuesFactory(Func factory) + => _storeGeneratedValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetTemporaryValuesFactory(Func factory) + => _temporaryValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetShadowValuesFactory(Func factory) + => _shadowValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual void SetEmptyShadowValuesFactory(Func factory) + => _emptyShadowValuesFactory = factory; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func OriginalValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _originalValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new OriginalValuesFactoryFactory().Create(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func StoreGeneratedValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _storeGeneratedValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new StoreGeneratedValuesFactoryFactory().CreateEmpty(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func TemporaryValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _temporaryValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new TemporaryValuesFactoryFactory().Create(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func ShadowValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _shadowValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new ShadowValuesFactoryFactory().Create(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 virtual Func EmptyShadowValuesFactory + => NonCapturingLazyInitializer.EnsureInitialized( + ref _emptyShadowValuesFactory, this, + static complexType => RuntimeFeature.IsDynamicCodeSupported + ? new EmptyShadowValuesFactoryFactory().CreateEmpty(complexType) + : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); } diff --git a/src/EFCore/Metadata/RuntimePropertyBase.cs b/src/EFCore/Metadata/RuntimePropertyBase.cs index 1a10e57e8de..c36c24cd4f0 100644 --- a/src/EFCore/Metadata/RuntimePropertyBase.cs +++ b/src/EFCore/Metadata/RuntimePropertyBase.cs @@ -116,7 +116,7 @@ PropertyIndexes IRuntimePropertyBase.PropertyIndexes ref _indexes, this, static property => { - var _ = ((IRuntimeTypeBase)property.DeclaringType).Counts; + _ = ((IRuntimeEntityType)((IRuntimeTypeBase)property.DeclaringType).ContainingEntityType).Counts; }); set => NonCapturingLazyInitializer.EnsureInitialized(ref _indexes, value); } diff --git a/src/EFCore/Metadata/RuntimeTypeBase.cs b/src/EFCore/Metadata/RuntimeTypeBase.cs index b54adbc18a2..a96c92f059a 100644 --- a/src/EFCore/Metadata/RuntimeTypeBase.cs +++ b/src/EFCore/Metadata/RuntimeTypeBase.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.Json; @@ -29,13 +27,6 @@ public abstract class RuntimeTypeBase : AnnotatableBase, IRuntimeTypeBase 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? _storeGeneratedValuesFactory; - private Func? _shadowValuesFactory; - private Func? _emptyShadowValuesFactory; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -134,15 +125,6 @@ protected virtual IEnumerable GetDerivedTypes() return derivedTypes; } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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] - protected abstract PropertyCounts Counts { get; } - /// /// Adds a property to this entity type. /// @@ -487,56 +469,6 @@ private IEnumerable FindDerivedComplexProperties(string /// public abstract IEnumerable FindMembersInHierarchy(string 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. - /// - [EntityFrameworkInternal] - public virtual void SetOriginalValuesFactory(Func factory) - => _originalValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetStoreGeneratedValuesFactory(Func factory) - => _storeGeneratedValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetTemporaryValuesFactory(Func factory) - => _temporaryValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetShadowValuesFactory(Func factory) - => _shadowValuesFactory = factory; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in 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 virtual void SetEmptyShadowValuesFactory(Func factory) - => _emptyShadowValuesFactory = factory; - /// /// Gets or sets the for the preferred constructor. /// @@ -672,53 +604,6 @@ IEnumerable IReadOnlyTypeBase.GetDerivedComplexPropert IReadOnlyComplexProperty? IReadOnlyTypeBase.FindDeclaredComplexProperty(string name) => FindDeclaredComplexProperty(name); - /// - PropertyCounts IRuntimeTypeBase.Counts - { - [DebuggerStepThrough] - get => Counts; - } - - /// - Func IRuntimeTypeBase.OriginalValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _originalValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new OriginalValuesFactoryFactory().Create(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.StoreGeneratedValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _storeGeneratedValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new StoreGeneratedValuesFactoryFactory().CreateEmpty(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.TemporaryValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _temporaryValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new TemporaryValuesFactoryFactory().Create(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.ShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _shadowValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new ShadowValuesFactoryFactory().Create(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - - /// - Func IRuntimeTypeBase.EmptyShadowValuesFactory - => NonCapturingLazyInitializer.EnsureInitialized( - ref _emptyShadowValuesFactory, this, - static complexType => RuntimeFeature.IsDynamicCodeSupported - ? new EmptyShadowValuesFactoryFactory().CreateEmpty(complexType) - : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel)); - /// [DebuggerStepThrough] ChangeTrackingStrategy IReadOnlyTypeBase.GetChangeTrackingStrategy() diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index ef382f4ff84..5bc400d52b1 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -3043,14 +3043,6 @@ public static string ValueCannotBeNull(object? property, object? entityType, obj GetString("ValueCannotBeNull", "0_property", "1_entityType", nameof(propertyType)), property, entityType, propertyType); - /// - /// Adding the complex property '{type}.{property}' isn't supported because it's of a value type '{propertyType}'. See https://github.com/dotnet/efcore/issues/9906 for more information. - /// - public static string ValueComplexType(object? type, object? property, object? propertyType) - => string.Format( - GetString("ValueComplexType", nameof(type), nameof(property), nameof(propertyType)), - type, property, propertyType); - /// /// Calling '{visitMethodName}' is not allowed. Visit the expression manually for the relevant part in the visitor. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 97c63e8e89b..02b81723a70 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1591,9 +1591,6 @@ The value for property '{1_entityType}.{0_property}' cannot be set to null because its type is '{propertyType}' which is not a nullable type. - - Adding the complex property '{type}.{property}' isn't supported because it's of a value type '{propertyType}'. See https://github.com/dotnet/efcore/issues/9906 for more information. - Calling '{visitMethodName}' is not allowed. Visit the expression manually for the relevant part in the visitor. diff --git a/src/EFCore/Update/UpdateEntryExtensions.cs b/src/EFCore/Update/UpdateEntryExtensions.cs index 33efe07bb6e..9e06e370a2d 100644 --- a/src/EFCore/Update/UpdateEntryExtensions.cs +++ b/src/EFCore/Update/UpdateEntryExtensions.cs @@ -124,57 +124,77 @@ public static string ToDebugString( if ((options & ChangeTrackerDebugStringOptions.IncludeProperties) != 0) { - foreach (var property in entry.EntityType.GetProperties()) + DumpProperties(entry.EntityType, indent + 2); + + void DumpProperties(ITypeBase structuralType, int tempIndent) { - builder.AppendLine().Append(indentString); + var tempIndentString = new string(' ', tempIndent); + foreach (var property in structuralType.GetProperties()) + { + builder.AppendLine().Append(tempIndentString); - var currentValue = entry.GetCurrentValue(property); - builder - .Append(" ") - .Append(property.Name) - .Append(": "); + var currentValue = entry.GetCurrentValue(property); + builder + .Append(" ") + .Append(property.Name) + .Append(": "); - AppendValue(currentValue); + AppendValue(currentValue); - if (property.IsPrimaryKey()) - { - builder.Append(" PK"); - } - else if (property.IsKey()) - { - builder.Append(" AK"); - } + if (property.IsPrimaryKey()) + { + builder.Append(" PK"); + } + else if (property.IsKey()) + { + builder.Append(" AK"); + } - if (property.IsForeignKey()) - { - builder.Append(" FK"); - } + if (property.IsForeignKey()) + { + builder.Append(" FK"); + } - if (entry.IsModified(property)) - { - builder.Append(" Modified"); - } + if (entry.IsModified(property)) + { + builder.Append(" Modified"); + } - if (entry.HasTemporaryValue(property)) - { - builder.Append(" Temporary"); - } + if (entry.HasTemporaryValue(property)) + { + builder.Append(" Temporary"); + } - if (entry.IsUnknown(property)) - { - builder.Append(" Unknown"); - } + if (entry.IsUnknown(property)) + { + builder.Append(" Unknown"); + } - if (entry.HasOriginalValuesSnapshot - && property.GetOriginalValueIndex() != -1) - { - var originalValue = entry.GetOriginalValue(property); - if (!Equals(originalValue, currentValue)) + if (entry.HasOriginalValuesSnapshot + && property.GetOriginalValueIndex() != -1) { - builder.Append(" Originally "); - AppendValue(originalValue); + var originalValue = entry.GetOriginalValue(property); + if (!Equals(originalValue, currentValue)) + { + builder.Append(" Originally "); + AppendValue(originalValue); + } } } + + foreach (var complexProperty in structuralType.GetComplexProperties()) + { + builder.AppendLine().Append(tempIndentString); + + builder + .Append(" ") + .Append(complexProperty.Name) + .Append(" (Complex: ") + .Append(complexProperty.ClrType.ShortDisplayName()) + .Append(")"); + + DumpProperties(complexProperty.ComplexType, tempIndent + 2); + } } } else diff --git a/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs new file mode 100644 index 00000000000..6cad13b626a --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingInMemoryTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingInMemoryTest(InMemoryFixture fixture) + : base(fixture) + { + } + + protected override void ExecuteWithStrategyInTransaction( + Action testOperation, + Action nestedTestOperation1 = null, + Action nestedTestOperation2 = null) + { + try + { + base.ExecuteWithStrategyInTransaction(testOperation, nestedTestOperation1, nestedTestOperation2); + } + finally + { + Fixture.Reseed(); + } + } + + protected override async Task ExecuteWithStrategyInTransactionAsync( + Func testOperation, + Func nestedTestOperation1 = null, + Func nestedTestOperation2 = null) + { + try + { + await base.ExecuteWithStrategyInTransactionAsync(testOperation, nestedTestOperation1, nestedTestOperation2); + } + finally + { + Fixture.Reseed(); + } + } + + public class InMemoryFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings(w => w.Log(InMemoryEventId.TransactionIgnoredWarning)); + } +} diff --git a/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs new file mode 100644 index 00000000000..370473c5d8e --- /dev/null +++ b/test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs @@ -0,0 +1,1632 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class ComplexTypesTrackingTestBase : IClassFixture + where TFixture : ComplexTypesTrackingTestBase.FixtureBase +{ + protected ComplexTypesTrackingTestBase(TFixture fixture) + { + Fixture = fixture; + } + + protected TFixture Fixture { get; } + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_objects(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreatePub()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_type_properties_modified(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreatePub()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_complex_types(bool trackFromQuery) + => ReadOriginalValuesTest(trackFromQuery, CreatePub()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_complex_types(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreatePub()); + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_structs(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreatePubWithStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_readonly_struct_properties_modified(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreatePubWithStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_structs(bool trackFromQuery) + => ReadOriginalValuesTest(trackFromQuery, CreatePubWithStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_structs(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreatePubWithStructs()); + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_readonly_structs(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreatePubWithReadonlyStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_readonly_readonly_struct_properties_modified(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreatePubWithReadonlyStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_readonly_structs(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreatePubWithReadonlyStructs()); + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_record_objects(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreatePubWithRecords()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_record_type_properties_modified(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreatePubWithRecords()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_record_complex_types(bool trackFromQuery) + => ReadOriginalValuesTest(trackFromQuery, CreatePubWithRecords()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_record_complex_types(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreatePubWithRecords()); + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_objects_with_fields(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreateFieldPub()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_type_properties_modified_with_fields(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreateFieldPub()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_complex_types_with_fields(bool trackFromQuery) + => ReadOriginalValuesTest(trackFromQuery, CreateFieldPub()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_complex_types_with_fields(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreateFieldPub()); + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_structs_with_fields(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreateFieldPubWithStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_readonly_struct_properties_modified_with_fields(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreateFieldPubWithStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_structs_with_fields(bool trackFromQuery) + => ReadOriginalValuesTest(trackFromQuery, CreateFieldPubWithStructs()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_structs_with_fields(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreateFieldPubWithStructs()); + + [ConditionalTheory(Skip = "Constructor binding")] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_readonly_structs_with_fields(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreateFieldPubWithReadonlyStructs()); + + [ConditionalTheory(Skip = "Constructor binding")] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_readonly_readonly_struct_properties_modified_with_fields(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreateFieldPubWithReadonlyStructs()); + + [ConditionalTheory(Skip = "Constructor binding")] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_readonly_structs_with_fields(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreateFieldPubWithReadonlyStructs()); + + [ConditionalTheory] + [InlineData(EntityState.Added, false)] + [InlineData(EntityState.Added, true)] + [InlineData(EntityState.Unchanged, false)] + [InlineData(EntityState.Unchanged, true)] + [InlineData(EntityState.Modified, false)] + [InlineData(EntityState.Modified, true)] + [InlineData(EntityState.Deleted, false)] + [InlineData(EntityState.Deleted, true)] + public virtual Task Can_track_entity_with_complex_record_objects_with_fields(EntityState state, bool async) + => TrackAndSaveTest(state, async, CreateFieldPubWithRecords()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_mark_complex_record_type_properties_modified_with_fields(bool trackFromQuery) + => MarkModifiedTest(trackFromQuery, CreateFieldPubWithRecords()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_read_original_values_for_properties_of_record_complex_types_with_fields(bool trackFromQuery) + => ReadOriginalValuesTest(trackFromQuery, CreateFieldPubWithRecords()); + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Can_write_original_values_for_properties_of_record_complex_types_with_fields(bool trackFromQuery) + => WriteOriginalValuesTest(trackFromQuery, CreateFieldPubWithRecords()); + + private async Task TrackAndSaveTest(EntityState state, bool async, TEntity pub) + where TEntity : class + => await ExecuteWithStrategyInTransactionAsync( + async context => + { + var entry = state switch + { + EntityState.Unchanged => context.Attach(pub), + EntityState.Deleted => context.Remove(pub), + EntityState.Modified => context.Update(pub), + EntityState.Added => async ? await context.AddAsync(pub) : context.Add(pub), + _ => throw new ArgumentOutOfRangeException(nameof(state), state, null) + }; + + Assert.Equal(state, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, state == EntityState.Modified); + + if (state == EntityState.Added || state == EntityState.Unchanged) + { + _ = async ? await context.SaveChangesAsync() : context.SaveChanges(); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + } + }); + + private void MarkModifiedTest(bool trackFromQuery, TEntity pub) + where TEntity : class + { + using var context = CreateContext(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + MarkModified(entry, "EveningActivity.RunnersUp.Members", true); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.RunnersUp.Members")); + + MarkModified(entry, "LunchtimeActivity.Day", true); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + + MarkModified(entry, "EveningActivity.CoverCharge", true); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + + Assert.False(IsModified(entry, "LunchtimeActivity.Name")); + Assert.False(IsModified(entry, "LunchtimeActivity.Description")); + Assert.False(IsModified(entry, "LunchtimeActivity.Notes")); + Assert.False(IsModified(entry, "LunchtimeActivity.CoverCharge")); + Assert.False(IsModified(entry, "LunchtimeActivity.IsTeamBased")); + Assert.False(IsModified(entry, "LunchtimeActivity.Champions.Name")); + Assert.False(IsModified(entry, "LunchtimeActivity.Champions.Members")); + Assert.False(IsModified(entry, "LunchtimeActivity.RunnersUp.Name")); + Assert.False(IsModified(entry, "LunchtimeActivity.RunnersUp.Members")); + Assert.False(IsModified(entry, "EveningActivity.Name")); + Assert.False(IsModified(entry, "EveningActivity.Day")); + Assert.False(IsModified(entry, "EveningActivity.Description")); + Assert.False(IsModified(entry, "EveningActivity.Notes")); + Assert.False(IsModified(entry, "EveningActivity.IsTeamBased")); + Assert.False(IsModified(entry, "EveningActivity.Champions.Name")); + Assert.False(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.False(IsModified(entry, "EveningActivity.RunnersUp.Name")); + Assert.False(IsModified(entry, "FeaturedTeam.Name")); + Assert.False(IsModified(entry, "FeaturedTeam.Members")); + + MarkModified(entry, "EveningActivity.RunnersUp.Members", false); + Assert.Equal(EntityState.Modified, entry.State); + Assert.False(IsModified(entry, "EveningActivity.RunnersUp.Members")); + + MarkModified(entry, "LunchtimeActivity.Day", false); + Assert.Equal(EntityState.Modified, entry.State); + Assert.False(IsModified(entry, "LunchtimeActivity.Day")); + + MarkModified(entry, "EveningActivity.CoverCharge", false); + Assert.Equal(EntityState.Unchanged, entry.State); + Assert.False(IsModified(entry, "EveningActivity.CoverCharge")); + + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + } + + private void ReadOriginalValuesTest(bool trackFromQuery, TEntity pub) + where TEntity : class + { + using var context = CreateContext(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + WriteCurrentValue(entry, "EveningActivity.Champions.Members", new List { "1", "2", "3" }); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + WriteCurrentValue(entry, "LunchtimeActivity.Day", DayOfWeek.Wednesday); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + WriteCurrentValue(entry, "EveningActivity.CoverCharge", 3.0m); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + private void WriteOriginalValuesTest(bool trackFromQuery, TEntity pub) + where TEntity : class + { + using var context = CreateContext(); + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + WriteOriginalValue(entry, "EveningActivity.Champions.Members", new List { "1", "2", "3" }); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "Robert", "Jimmy", "John", "Jason" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + WriteOriginalValue(entry, "LunchtimeActivity.Day", DayOfWeek.Wednesday); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + WriteOriginalValue(entry, "EveningActivity.CoverCharge", 3.0m); + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Detect_changes_in_complex_type_properties(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePub(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + pub.EveningActivity.Champions.Members = new List + { + "1", + "2", + "3" + }; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + pub.LunchtimeActivity.Day = DayOfWeek.Wednesday; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + pub.EveningActivity.CoverCharge = 3.0m; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Detect_changes_in_complex_struct_type_properties(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePubWithStructs(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + var eveningActivity = pub.EveningActivity; + var champions = eveningActivity.Champions; + champions.Members = new() + { + "1", + "2", + "3" + }; + eveningActivity.Champions = champions; + pub.EveningActivity = eveningActivity; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + var lunchtimeActivity = pub.LunchtimeActivity; + lunchtimeActivity.Day = DayOfWeek.Wednesday; + pub.LunchtimeActivity = lunchtimeActivity; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + eveningActivity = pub.EveningActivity; + eveningActivity.CoverCharge = 3.0m; + pub.EveningActivity = eveningActivity; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Detects_changes_in_complex_readonly_struct_type_properties(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePubWithReadonlyStructs(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + pub.EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "1", + "2", + "3" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + pub.LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Wednesday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = new() + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + pub.EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 3.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "1", + "2", + "3" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual void Detects_changes_in_complex_record_type_properties(bool trackFromQuery) + { + using var context = CreateContext(); + var pub = CreatePubWithRecords(); + + var entry = trackFromQuery ? TrackFromQuery(context, pub) : context.Attach(pub); + + Assert.Equal(EntityState.Unchanged, entry.State); + AssertPropertyValues(entry); + AssertPropertiesModified(entry, false); + + pub.EveningActivity = pub.EveningActivity with + { + Champions = pub.EveningActivity.Champions with + { + Members = new() + { + "1", + "2", + "3" + } + } + }; + + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(new[] { "1", "2", "3" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadOriginalValue>(entry, "EveningActivity.Champions.Members")); + + pub.LunchtimeActivity = pub.LunchtimeActivity with { Day = DayOfWeek.Wednesday }; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Wednesday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal(DayOfWeek.Monday, ReadOriginalValue(entry, "LunchtimeActivity.Day")); + + pub.EveningActivity = pub.EveningActivity with { CoverCharge = 3.0m }; + context.ChangeTracker.DetectChanges(); + + Assert.Equal(EntityState.Modified, entry.State); + Assert.True(IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(3.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.Equal(5.0m, ReadOriginalValue(entry, "EveningActivity.CoverCharge")); + } + + protected void AssertPropertyValues(EntityEntry entry) + { + Assert.Equal("The FBI", ReadCurrentValue(entry, "Name")); + Assert.Equal("Pub Quiz", ReadCurrentValue(entry, "LunchtimeActivity.Name")); + Assert.Equal(DayOfWeek.Monday, ReadCurrentValue(entry, "LunchtimeActivity.Day")); + Assert.Equal("A general knowledge pub quiz.", ReadCurrentValue(entry, "LunchtimeActivity.Description")); + Assert.Equal(new[] { "One", "Two", "Three" }, ReadCurrentValue(entry, "LunchtimeActivity.Notes")); + Assert.Equal(2.0m, ReadCurrentValue(entry, "LunchtimeActivity.CoverCharge")); + Assert.True(ReadCurrentValue(entry, "LunchtimeActivity.IsTeamBased")); + Assert.Equal("Clueless", ReadCurrentValue(entry, "LunchtimeActivity.Champions.Name")); + Assert.Equal(new[] { "Boris", "David", "Theresa" }, ReadCurrentValue>(entry, "LunchtimeActivity.Champions.Members")); + Assert.Equal("ZZ", ReadCurrentValue(entry, "LunchtimeActivity.RunnersUp.Name")); + Assert.Equal( + new[] { "Has Beard", "Has Beard", "Is Called Beard" }, + ReadCurrentValue>(entry, "LunchtimeActivity.RunnersUp.Members")); + Assert.Equal("Music Quiz", ReadCurrentValue(entry, "EveningActivity.Name")); + Assert.Equal(DayOfWeek.Friday, ReadCurrentValue(entry, "EveningActivity.Day")); + Assert.Equal("A music pub quiz.", ReadCurrentValue(entry, "EveningActivity.Description")); + Assert.Empty(ReadCurrentValue(entry, "EveningActivity.Notes")); + Assert.Equal(5.0m, ReadCurrentValue(entry, "EveningActivity.CoverCharge")); + Assert.True(ReadCurrentValue(entry, "EveningActivity.IsTeamBased")); + Assert.Equal("Dazed and Confused", ReadCurrentValue(entry, "EveningActivity.Champions.Name")); + Assert.Equal( + new[] { "Robert", "Jimmy", "John", "Jason" }, ReadCurrentValue>(entry, "EveningActivity.Champions.Members")); + Assert.Equal("Banksy", ReadCurrentValue(entry, "EveningActivity.RunnersUp.Name")); + Assert.Empty(ReadCurrentValue>(entry, "EveningActivity.RunnersUp.Members")); + Assert.Equal("Not In This Lifetime", ReadCurrentValue(entry, "FeaturedTeam.Name")); + Assert.Equal(new[] { "Slash", "Axl" }, ReadCurrentValue>(entry, "FeaturedTeam.Members")); + } + + protected void AssertPropertiesModified(EntityEntry entry, bool expected) + { + Assert.Equal(expected, IsModified(entry, "Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Day")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Description")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Notes")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.CoverCharge")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.IsTeamBased")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Champions.Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.Champions.Members")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.RunnersUp.Name")); + Assert.Equal(expected, IsModified(entry, "LunchtimeActivity.RunnersUp.Members")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Name")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Day")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Description")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Notes")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.CoverCharge")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.IsTeamBased")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Champions.Name")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.Champions.Members")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.RunnersUp.Name")); + Assert.Equal(expected, IsModified(entry, "EveningActivity.RunnersUp.Members")); + Assert.Equal(expected, IsModified(entry, "FeaturedTeam.Name")); + Assert.Equal(expected, IsModified(entry, "FeaturedTeam.Members")); + } + + protected static TValue ReadCurrentValue(EntityEntry entry, string propertyChain) + => entry.GetInfrastructure().GetCurrentValue(FindProperty(entry, propertyChain)); + + protected static TValue ReadOriginalValue(EntityEntry entry, string propertyChain) + => entry.GetInfrastructure().GetOriginalValue((IProperty)FindProperty(entry, propertyChain)); + + protected static void WriteCurrentValue(EntityEntry entry, string propertyChain, object? value) + => entry.GetInfrastructure().SetProperty(FindProperty(entry, propertyChain), value, isMaterialization: false); + + protected static void WriteOriginalValue(EntityEntry entry, string propertyChain, object? value) + => entry.GetInfrastructure().SetOriginalValue(FindProperty(entry, propertyChain), value); + + protected static bool IsModified(EntityEntry entry, string propertyChain) + => entry.GetInfrastructure().IsModified((IProperty)FindProperty(entry, propertyChain)); + + protected static EntityEntry TrackFromQuery(DbContext context, TEntity pub) + where TEntity : class + => new( + context.GetService().StartTrackingFromQuery( + context.Model.FindEntityType(typeof(TEntity))!, pub, new ValueBuffer())); + + protected static void MarkModified(EntityEntry entry, string propertyChain, bool modified) + => entry.GetInfrastructure().SetPropertyModified((IProperty)FindProperty(entry, propertyChain), isModified: modified); + + protected static IPropertyBase FindProperty(EntityEntry entry, string propertyChain) + { + var internalEntry = entry.GetInfrastructure(); + var names = propertyChain.Split("."); + var currentType = (ITypeBase)internalEntry.EntityType; + + IPropertyBase property = null!; + foreach (var name in names) + { + var complexProperty = currentType.FindComplexProperty(name); + if (complexProperty != null) + { + currentType = complexProperty.ComplexType; + property = complexProperty; + } + else + { + property = currentType.FindProperty(name)!; + } + } + + return property; + } + + protected virtual void ExecuteWithStrategyInTransaction( + Action testOperation, + Action? nestedTestOperation1 = null, + Action? nestedTestOperation2 = null) + => TestHelpers.ExecuteWithStrategyInTransaction( + CreateContext, UseTransaction, + testOperation, nestedTestOperation1, nestedTestOperation2); + + protected virtual Task ExecuteWithStrategyInTransactionAsync( + Func testOperation, + Func? nestedTestOperation1 = null, + Func? nestedTestOperation2 = null) + => TestHelpers.ExecuteWithStrategyInTransactionAsync( + CreateContext, UseTransaction, + testOperation, nestedTestOperation1, nestedTestOperation2); + + protected virtual void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + { + } + + protected DbContext CreateContext() + => Fixture.CreateContext(); + + public abstract class FixtureBase : SharedStoreFixtureBase + { + protected override string StoreName + => "ComplexTypesTrackingTest"; + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + + // TODO: Allow binding of complex properties to constructors + // modelBuilder.Entity( + // b => + // { + // b.ComplexProperty( + // e => e.LunchtimeActivity, b => + // { + // b.ComplexProperty(e => e!.Champions); + // b.ComplexProperty(e => e!.RunnersUp); + // }); + // b.ComplexProperty( + // e => e.EveningActivity, b => + // { + // b.ComplexProperty(e => e.Champions); + // b.ComplexProperty(e => e.RunnersUp); + // }); + // b.ComplexProperty(e => e.FeaturedTeam); + // b.ComplexProperty(e => e.FeaturedTeam); + // }); + + modelBuilder.Entity( + b => + { + b.ComplexProperty( + e => e.LunchtimeActivity, b => + { + b.ComplexProperty(e => e!.Champions); + b.ComplexProperty(e => e!.RunnersUp); + }); + b.ComplexProperty( + e => e.EveningActivity, b => + { + b.ComplexProperty(e => e.Champions); + b.ComplexProperty(e => e.RunnersUp); + }); + b.ComplexProperty(e => e.FeaturedTeam); + b.ComplexProperty(e => e.FeaturedTeam); + }); + } + } + + protected static Pub CreatePub() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() + { + Name = "Banksy", Members = new() + }, + }, + FeaturedTeam = new() + { + Name = "Not In This Lifetime", + Members = + { + "Slash", + "Axl" + } + } + }; + + protected class Pub + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public Activity LunchtimeActivity { get; set; } = null!; + public Activity EveningActivity { get; set; } = null!; + public Team FeaturedTeam { get; set; } = null!; + } + + protected class Activity + { + public string Name { get; set; } = null!; + public decimal? CoverCharge { get; set; } + public bool IsTeamBased { get; set; } + public string? Description { get; set; } + public string[]? Notes { get; set; } + public DayOfWeek Day { get; set; } + public Team Champions { get; set; } = null!; + public Team RunnersUp { get; set; } = null!; + } + + protected class Team + { + public string Name { get; set; } = null!; + public List Members { get; set; } = new(); + } + + protected static PubWithStructs CreatePubWithStructs() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = new() + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }, + FeaturedTeam = new() { Name = "Not In This Lifetime", Members = new() { "Slash", "Axl" } } + }; + + protected class PubWithStructs + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public ActivityStruct LunchtimeActivity { get; set; } + public ActivityStruct EveningActivity { get; set; } + public TeamStruct FeaturedTeam { get; set; } + } + + protected struct ActivityStruct + { + public string Name { get; set; } + public decimal? CoverCharge { get; set; } + public bool IsTeamBased { get; set; } + public string? Description { get; set; } + public string[]? Notes { get; set; } + public DayOfWeek Day { get; set; } + public TeamStruct Champions { get; set; } + public TeamStruct RunnersUp { get; set; } + } + + protected struct TeamStruct + { + public string Name { get; set; } + public List Members { get; set; } + } + + protected static PubWithReadonlyStructs CreatePubWithReadonlyStructs() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = new() + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }, + FeaturedTeam = new() { Name = "Not In This Lifetime", Members = new() { "Slash", "Axl" } } + }; + + protected class PubWithReadonlyStructs + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public ActivityReadonlyStruct LunchtimeActivity { get; set; } + public ActivityReadonlyStruct EveningActivity { get; set; } + public TeamReadonlyStruct FeaturedTeam { get; set; } + } + + protected readonly struct ActivityReadonlyStruct + { + public string Name { get; init; } + public decimal? CoverCharge { get; init; } + public bool IsTeamBased { get; init; } + public string? Description { get; init; } + public string[]? Notes { get; init; } + public DayOfWeek Day { get; init; } + public TeamReadonlyStruct Champions { get; init; } + public TeamReadonlyStruct RunnersUp { get; init; } + } + + protected readonly struct TeamReadonlyStruct + { + public string Name { get; init; } + public List Members { get; init; } + } + + protected static PubWithRecords CreatePubWithRecords() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = new() + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }, + FeaturedTeam = new() { Name = "Not In This Lifetime", Members = new() { "Slash", "Axl" } } + }; + + protected class PubWithRecords + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public ActivityRecord LunchtimeActivity { get; set; } = null!; + public ActivityRecord EveningActivity { get; set; } = null!; + public TeamRecord FeaturedTeam { get; set; } = null!; + } + + protected record ActivityRecord + { + public string Name { get; init; } = null!; + public decimal? CoverCharge { get; init; } + public bool IsTeamBased { get; init; } + public string? Description { get; init; } + public string[]? Notes { get; init; } + public DayOfWeek Day { get; init; } + public TeamRecord Champions { get; init; } = null!; + public TeamRecord RunnersUp { get; init; } = null!; + } + + protected record TeamRecord + { + public string Name { get; init; } = null!; + public List Members { get; init; } = null!; + } + + protected static FieldPub CreateFieldPub() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() + { + Name = "Banksy", Members = new() + }, + }, + FeaturedTeam = new() + { + Name = "Not In This Lifetime", + Members = + { + "Slash", + "Axl" + } + } + }; + + protected class FieldPub + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public FieldActivity LunchtimeActivity = null!; + public FieldActivity EveningActivity = null!; + public FieldTeam FeaturedTeam = null!; + } + + protected class FieldActivity + { + public string Name = null!; + public decimal? CoverCharge; + public bool IsTeamBased; + public string? Description; + public string[]? Notes; + public DayOfWeek Day; + public Team Champions = null!; + public Team RunnersUp = null!; + } + + protected class FieldTeam + { + public string Name = null!; + public List Members = new(); + } + + protected static FieldPubWithStructs CreateFieldPubWithStructs() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = new() + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }, + FeaturedTeam = new() { Name = "Not In This Lifetime", Members = new() { "Slash", "Axl" } } + }; + + protected class FieldPubWithStructs + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public FieldActivityStruct LunchtimeActivity; + public FieldActivityStruct EveningActivity; + public FieldTeamStruct FeaturedTeam; + } + + protected struct FieldActivityStruct + { + public string Name; + public decimal? CoverCharge; + public bool IsTeamBased; + public string? Description; + public string[]? Notes; + public DayOfWeek Day; + public FieldTeamStruct Champions; + public FieldTeamStruct RunnersUp; + } + + protected struct FieldTeamStruct + { + public string Name; + public List Members; + } + + protected static FieldPubWithReadonlyStructs CreateFieldPubWithReadonlyStructs() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = + new( + "Pub Quiz", 2.0m, true, "A general knowledge pub quiz.", new[] { "One", "Two", "Three" }, DayOfWeek.Monday, + new( + "Clueless", new() + { + "Boris", + "David", + "Theresa" + }), new( + "ZZ", new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + })), + EveningActivity = + new( + "Music Quiz", 5.0m, true, "A music pub quiz.", Array.Empty(), DayOfWeek.Friday, + new( + "Dazed and Confused", new() + { + "Robert", + "Jimmy", + "John", + "Jason" + }), new("Banksy", new())), + FeaturedTeam = new("Not In This Lifetime", new() { "Slash", "Axl" }) + }; + + protected class FieldPubWithReadonlyStructs + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public FieldActivityReadonlyStruct LunchtimeActivity; + public FieldActivityReadonlyStruct EveningActivity; + public FieldTeamReadonlyStruct FeaturedTeam; + } + + protected readonly struct FieldActivityReadonlyStruct( + string name, + decimal? coverCharge, + bool isTeamBased, + string? description, + string[]? notes, + DayOfWeek day, + FieldTeamReadonlyStruct champions, + FieldTeamReadonlyStruct runnersUp) + { + public readonly string Name = name; + public readonly decimal? CoverCharge = coverCharge; + public readonly bool IsTeamBased = isTeamBased; + public readonly string? Description = description; + public readonly string[]? Notes = notes; + public readonly DayOfWeek Day = day; + public readonly FieldTeamReadonlyStruct Champions = champions; + public readonly FieldTeamReadonlyStruct RunnersUp = runnersUp; + } + + protected readonly struct FieldTeamReadonlyStruct(string name, List members) + { + public readonly string Name = name; + public readonly List Members = members; + } + + protected static FieldPubWithRecords CreateFieldPubWithRecords() + => new() + { + Id = Guid.NewGuid(), + Name = "The FBI", + LunchtimeActivity = new() + { + Name = "Pub Quiz", + Day = DayOfWeek.Monday, + Description = "A general knowledge pub quiz.", + Notes = new[] { "One", "Two", "Three" }, + CoverCharge = 2.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Clueless", + Members = new() + { + "Boris", + "David", + "Theresa" + } + }, + RunnersUp = new() + { + Name = "ZZ", + Members = new() + { + "Has Beard", + "Has Beard", + "Is Called Beard" + } + }, + }, + EveningActivity = new() + { + Name = "Music Quiz", + Day = DayOfWeek.Friday, + Description = "A music pub quiz.", + Notes = Array.Empty(), + CoverCharge = 5.0m, + IsTeamBased = true, + Champions = new() + { + Name = "Dazed and Confused", + Members = new() + { + "Robert", + "Jimmy", + "John", + "Jason" + } + }, + RunnersUp = new() { Name = "Banksy", Members = new() } + }, + FeaturedTeam = new() { Name = "Not In This Lifetime", Members = new() { "Slash", "Axl" } } + }; + + protected class FieldPubWithRecords + { + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public FieldActivityRecord LunchtimeActivity = null!; + public FieldActivityRecord EveningActivity = null!; + public FieldTeamRecord FeaturedTeam = null!; + } + + protected record FieldActivityRecord + { + public string Name = null!; + public decimal? CoverCharge; + public bool IsTeamBased; + public string? Description; + public string[]? Notes; + public DayOfWeek Day; + public FieldTeamRecord Champions = null!; + public FieldTeamRecord RunnersUp = null!; + } + + protected record FieldTeamRecord + { + public string Name = null!; + public List Members = null!; + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs new file mode 100644 index 00000000000..d75f7332f12 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingSqlServerTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingSqlServerTest(SqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class SqlServerFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs new file mode 100644 index 00000000000..980ff315e9a --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ComplexTypesTrackingSqliteTest.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore; + +public class ComplexTypesTrackingSqliteTest : ComplexTypesTrackingTestBase +{ + public ComplexTypesTrackingSqliteTest(SqliteFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + fixture.TestSqlLoggerFactory.Clear(); + fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + + public class SqliteFixture : FixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } +} diff --git a/test/EFCore.Tests/DbContextServicesTest.cs b/test/EFCore.Tests/DbContextServicesTest.cs index ea997c27bd7..1cb32bc558d 100644 --- a/test/EFCore.Tests/DbContextServicesTest.cs +++ b/test/EFCore.Tests/DbContextServicesTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; @@ -778,11 +780,34 @@ public void Can_get_replaced_singleton_service_from_scoped_configuration() Assert.IsType(context.GetService()); } + [ComplexType] + private class Tag + { + public string Name { get; set; } + + [Required] + public Stamp Stamp { get; set; } + + public string[] Notes { get; set; } + } + + [ComplexType] + private class Stamp + { + public Guid Code { get; set; } + } + private class Category { public int Id { get; set; } public string Name { get; set; } + [Required] + public Tag Tag { get; set; } + + [Required] + public Stamp Stamp { get; set; } + public List Products { get; set; } } @@ -792,6 +817,12 @@ private class Product public string Name { get; set; } public decimal Price { get; set; } + [Required] + public Tag Tag { get; set; } + + [Required] + public Stamp Stamp { get; set; } + public int CategoryId { get; set; } public Category Category { get; set; } } diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 06cdb176e39..589e4725850 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -51,7 +51,20 @@ public void Local_calls_DetectChanges() changeDetector.DetectChangesCalled = false; var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Big Hedgehogs"; @@ -78,7 +91,20 @@ public void Local_does_not_call_DetectChanges_when_disabled() changeDetector.DetectChangesCalled = false; var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Big Hedgehogs"; @@ -508,7 +534,20 @@ public async Task SaveChanges_calls_DetectChanges_by_default(bool async) Assert.True(context.ChangeTracker.AutoDetectChangesEnabled); var product = (await context.AddAsync( - new Product { Id = 1, Name = "Little Hedgehogs" })).Entity; + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } })).Entity; if (async) { @@ -550,7 +589,20 @@ public async Task Auto_DetectChanges_for_SaveChanges_can_be_switched_off(bool as Assert.False(context.ChangeTracker.AutoDetectChangesEnabled); var product = (await context.AddAsync( - new Product { Id = 1, Name = "Little Hedgehogs" })).Entity; + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } })).Entity; if (async) { @@ -611,8 +663,47 @@ public void DetectChanges_is_called_for_cascade_delete_unless_disabled(bool auto context.ChangeTracker.AutoDetectChangesEnabled = autoDetectChangesEnabled; - var products = new List { new() { Id = 1 }, new() { Id = 2 } }; - var category = context.Attach(new Category { Id = 1, Products = products }).Entity; + var products = new List { new() { Id = 1, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }, new() { Id = 2, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }; + var category = context.Attach(new Category { Id = 1, Products = products, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; Assert.Empty(detectedChangesFor); @@ -638,7 +729,20 @@ public void Entry_calls_DetectChanges_by_default(bool useGenericOverload) { using var context = new ButTheHedgehogContext(InMemoryTestHelpers.Instance.CreateServiceProvider()); var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Cracked Cookies"; @@ -665,7 +769,20 @@ public void Auto_DetectChanges_for_Entry_can_be_switched_off(bool useGenericOver context.ChangeTracker.AutoDetectChangesEnabled = false; var entry = context.Attach( - new Product { Id = 1, Name = "Little Hedgehogs" }); + new Product { Id = 1, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); entry.Entity.Name = "Cracked Cookies"; @@ -697,65 +814,455 @@ public async Task Add_Attach_Remove_Update_do_not_call_DetectChanges() changeDetector.DetectChangesCalled = false; context.Add( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Add( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AddRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AddRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AddRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.AddRange( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); await context.AddAsync( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddAsync( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddRangeAsync( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddRangeAsync( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); await context.AddRangeAsync( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); await context.AddRangeAsync( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.Attach( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Attach( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AttachRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AttachRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.AttachRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.AttachRange( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.Update( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Update( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.UpdateRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.UpdateRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.UpdateRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.UpdateRange( - new List { new Product { Id = id++, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.Remove( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.Remove( - (object)new Product { Id = id++, Name = "Little Hedgehogs" }); + (object)new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.RemoveRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.RemoveRange( - new Product { Id = id++, Name = "Little Hedgehogs" }); + new Product { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }); context.RemoveRange( - new List { new() { Id = id++, Name = "Little Hedgehogs" } }); + new List { new() { Id = id++, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); context.RemoveRange( - new List { new Product { Id = id, Name = "Little Hedgehogs" } }); + new List { new Product { Id = id, Name = "Little Hedgehogs", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } } }); Assert.False(changeDetector.DetectChangesCalled); diff --git a/test/EFCore.Tests/DbContextTrackingTest.cs b/test/EFCore.Tests/DbContextTrackingTest.cs index b73699f94e7..b2424175a3b 100644 --- a/test/EFCore.Tests/DbContextTrackingTest.cs +++ b/test/EFCore.Tests/DbContextTrackingTest.cs @@ -44,22 +44,74 @@ private static async Task TrackEntitiesTest( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principalEntry = await categoryAdder(context, principal); @@ -129,22 +181,74 @@ private static async Task TrackMultipleEntitiesTest( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await adder(context, new object[] { principal, dependent }); @@ -202,12 +306,38 @@ private static async Task TrackEntitiesDefaultValueTest( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var categoryEntry1 = await categoryAdder(context, category1); @@ -346,12 +476,38 @@ private static async Task TrackMultipleEntitiesDefaultValuesTest( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await categoryAdder(context, new[] { category1 }); @@ -439,22 +595,74 @@ private static async Task TrackEntitiesTestNonGeneric( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principalEntry = await categoryAdder(context, principal); @@ -524,22 +732,74 @@ private static async Task TrackMultipleEntitiesTestEnumerable( { Id = 1, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var principal = new Category { Id = 1, Name = "Beverages", - Products = new List { relatedDependent } + Products = new List { relatedDependent }, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; - var relatedPrincipal = new Category { Id = 2, Name = "Foods" }; + var relatedPrincipal = new Category { Id = 2, Name = "Foods", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var dependent = new Product { Id = 2, Name = "Bovril", Price = 4.99m, - Category = relatedPrincipal + Category = relatedPrincipal, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await adder(context, new object[] { principal, dependent }); @@ -597,12 +857,38 @@ private static async Task TrackEntitiesDefaultValuesTestNonGeneric( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var categoryEntry1 = await categoryAdder(context, category1); @@ -669,12 +955,38 @@ private static async Task TrackMultipleEntitiesDefaultValueTestEnumerable( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category1 = new Category { Id = 0, Name = "Beverages" }; + var category1 = new Category { Id = 0, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product1 = new Product { Id = 0, Name = "Marmite", - Price = 7.99m + Price = 7.99m, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; await categoryAdder( @@ -848,7 +1160,20 @@ private async Task ChangeStateWithMethod( EntityState expectedState) { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var entity = new Category { Id = 1, Name = "Beverages" }; + var entity = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var entry = context.Entry(entity); entry.State = initialState; @@ -862,13 +1187,39 @@ private async Task ChangeStateWithMethod( public void Can_attach_with_inconsistent_FK_principal_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -895,13 +1246,39 @@ public void Can_attach_with_inconsistent_FK_principal_first_fully_fixed_up() public void Can_attach_with_inconsistent_FK_dependent_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -926,13 +1303,39 @@ public void Can_attach_with_inconsistent_FK_dependent_first_fully_fixed_up() public void Can_attach_with_inconsistent_FK_principal_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -957,13 +1360,39 @@ public void Can_attach_with_inconsistent_FK_principal_first_collection_not_fixed public void Can_attach_with_inconsistent_FK_dependent_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -988,12 +1417,38 @@ public void Can_attach_with_inconsistent_FK_dependent_first_collection_not_fixed public void Can_attach_with_inconsistent_FK_principal_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1018,12 +1473,38 @@ public void Can_attach_with_inconsistent_FK_principal_first_reference_not_fixed_ public void Can_attach_with_inconsistent_FK_dependent_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1048,13 +1529,39 @@ public void Can_attach_with_inconsistent_FK_dependent_first_reference_not_fixed_ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1081,13 +1588,39 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_fully_ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_fully_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1112,13 +1645,39 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_fully_ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1143,13 +1702,39 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_collec public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_collection_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1174,12 +1759,38 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_collec public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1204,12 +1815,38 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_refere public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_reference_not_fixed_up() { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1235,15 +1872,54 @@ public void Can_attach_with_inconsistent_FK_principal_first_fully_fixed_up_with_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1272,16 +1948,55 @@ public void Can_attach_with_inconsistent_FK_dependent_first_fully_fixed_up_with_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1314,14 +2029,40 @@ public void Can_attach_with_inconsistent_FK_principal_first_collection_not_fixed var category7 = context.Attach( new Category { Id = 7, Products = new List() }).Entity; - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1354,14 +2095,40 @@ public void Can_attach_with_inconsistent_FK_dependent_first_collection_not_fixed var category7 = context.Attach( new Category { Id = 7, Products = new List() }).Entity; - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1394,12 +2161,38 @@ public void Can_attach_with_inconsistent_FK_principal_first_reference_not_fixed_ var category7 = context.Attach( new Category { Id = 7, Products = new List() }).Entity; - var category = new Category { Id = 1, Name = "Beverages" }; + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1427,14 +2220,53 @@ public void Can_attach_with_inconsistent_FK_dependent_first_reference_not_fixed_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1461,15 +2293,54 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_fully_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1497,16 +2368,55 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_fully_ { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1537,15 +2447,54 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_collec { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1574,16 +2523,55 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_collec { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, Name = "Marmite", - Category = category + Category = category, + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List(); @@ -1614,14 +2602,53 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_principal_first_refere { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; @@ -1649,14 +2676,53 @@ public void Can_set_set_to_Unchanged_with_inconsistent_FK_dependent_first_refere { using var context = new EarlyLearningCenter(InMemoryTestHelpers.Instance.CreateServiceProvider()); var category7 = context.Attach( - new Category { Id = 7, Products = new List() }).Entity; - - var category = new Category { Id = 1, Name = "Beverages" }; + new Category { Id = 7, Products = new List(), + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }).Entity; + + var category = new Category { Id = 1, Name = "Beverages", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; var product = new Product { Id = 1, CategoryId = 7, - Name = "Marmite" + Name = "Marmite", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7146") + }, + Tag = new() + { + Name = "Tanavast", + Stamp = new() + { + Code = new Guid("984ade3c-2f7b-4651-a351-642e92ab7147") + }, + Notes = new[] { "A", "B" } + } }; category.Products = new List { product }; diff --git a/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs b/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs index a14ff66393d..caaf6931743 100644 --- a/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs +++ b/test/EFCore.Tests/ModelBuilding/ComplexTypeTestBase.cs @@ -378,11 +378,11 @@ public virtual void Properties_can_be_made_concurrency_tokens() Assert.False(complexType.FindProperty("Bottom").IsConcurrencyToken); Assert.Equal(-1, complexType.FindProperty(Customer.IdProperty.Name).GetOriginalValueIndex()); - Assert.Equal(2, complexType.FindProperty("Up").GetOriginalValueIndex()); + Assert.Equal(6, complexType.FindProperty("Up").GetOriginalValueIndex()); Assert.Equal(-1, complexType.FindProperty("Down").GetOriginalValueIndex()); - Assert.Equal(0, complexType.FindProperty("Charm").GetOriginalValueIndex()); + Assert.Equal(4, complexType.FindProperty("Charm").GetOriginalValueIndex()); Assert.Equal(-1, complexType.FindProperty("Strange").GetOriginalValueIndex()); - Assert.Equal(1, complexType.FindProperty("Top").GetOriginalValueIndex()); + Assert.Equal(5, complexType.FindProperty("Top").GetOriginalValueIndex()); Assert.Equal(-1, complexType.FindProperty("Bottom").GetOriginalValueIndex()); Assert.Equal(ChangeTrackingStrategy.ChangingAndChangedNotifications, complexType.GetChangeTrackingStrategy()); @@ -1545,39 +1545,42 @@ public virtual void Complex_properties_not_discovered_by_convention() .Entity() .ComplexProperty(e => e.Customer); - // TODO: Issue #14661 - //modelBuilder - // .Entity() - // .Ignore(e => e.Tuple) - // .ComplexProperty(e => e.Label); + modelBuilder + .Entity( + b => + { + b.Ignore(e => e.Tuple); + b.ComplexProperty(e => e.Label, b => b.ComplexProperty(e => e.Customer)); + b.ComplexProperty(e => e.OldLabel, b => b.ComplexProperty(e => e.Customer)); + }); var model = modelBuilder.FinalizeModel(); - var customerType = model.FindEntityType(typeof(ComplexProperties)) - .FindComplexProperty(nameof(ComplexProperties.Customer)).ComplexType; - var indexedType = model.FindEntityType(typeof(ComplexProperties)) - .FindComplexProperty(nameof(ComplexProperties.IndexedClass)).ComplexType; + var customerType = model.FindEntityType(typeof(ComplexProperties))! + .FindComplexProperty(nameof(ComplexProperties.Customer))!.ComplexType; + var indexedType = model.FindEntityType(typeof(ComplexProperties))! + .FindComplexProperty(nameof(ComplexProperties.IndexedClass))!.ComplexType; - //var valueType = model.FindEntityType(typeof(ValueComplexProperties)); - //var labelProperty = valueType.FindComplexProperty(nameof(ValueComplexProperties.Label)); - //Assert.False(labelProperty.IsNullable); - //Assert.Equal(typeof(ProductLabel), labelProperty.ClrType); - //var labelType = labelProperty.ComplexType; - //Assert.Equal(typeof(ProductLabel), labelType.ClrType); + var valueType = model.FindEntityType(typeof(ValueComplexProperties))!; + var labelProperty = valueType.FindComplexProperty(nameof(ValueComplexProperties.Label))!; + Assert.False(labelProperty.IsNullable); + Assert.Equal(typeof(ProductLabel), labelProperty.ClrType); + var labelType = labelProperty.ComplexType; + Assert.Equal(typeof(ProductLabel), labelType.ClrType); - //var labelCustomerProperty = labelType.FindComplexProperty(nameof(ProductLabel.Customer)); - //Assert.True(labelCustomerProperty.IsNullable); - //Assert.Equal(typeof(Customer), labelCustomerProperty.ClrType); + var labelCustomerProperty = labelType.FindComplexProperty(nameof(ProductLabel.Customer))!; + Assert.False(labelCustomerProperty.IsNullable); + Assert.Equal(typeof(Customer), labelCustomerProperty.ClrType); - //var oldLabelProperty = valueType.FindComplexProperty(nameof(ValueComplexProperties.OldLabel)); - //Assert.True(oldLabelProperty.IsNullable); - //Assert.Equal(typeof(ProductLabel?), oldLabelProperty.ClrType); - //var oldLabelType = oldLabelProperty.ComplexType; - //Assert.Equal(typeof(ProductLabel), oldLabelType.ClrType); + var oldLabelProperty = valueType.FindComplexProperty(nameof(ValueComplexProperties.OldLabel))!; + Assert.False(oldLabelProperty.IsNullable); + Assert.Equal(typeof(ProductLabel), oldLabelProperty.ClrType); + var oldLabelType = oldLabelProperty.ComplexType; + Assert.Equal(typeof(ProductLabel), oldLabelType.ClrType); - //var oldLabelCustomerProperty = labelType.FindComplexProperty(nameof(ProductLabel.Customer)); - //Assert.True(oldLabelCustomerProperty.IsNullable); - //Assert.Equal(typeof(Customer), oldLabelCustomerProperty.ClrType); + var oldLabelCustomerProperty = labelType.FindComplexProperty(nameof(ProductLabel.Customer))!; + Assert.False(oldLabelCustomerProperty.IsNullable); + Assert.Equal(typeof(Customer), oldLabelCustomerProperty.ClrType); } [ConditionalFact] @@ -1624,24 +1627,17 @@ public virtual void Throws_for_tuple() .Ignore(e => e.OldLabel) .ComplexProperty(e => e.Tuple); - Assert.Equal( - CoreStrings.ValueComplexType( - nameof(ValueComplexProperties), nameof(ValueComplexProperties.Tuple), typeof((string, int)).ShortDisplayName()), - Assert.Throws(modelBuilder.FinalizeModel).Message); - - // Uncomment when value types are supported. - // TODO: Issue #14661 - //var model = modelBuilder.FinalizeModel(); + var model = modelBuilder.FinalizeModel(); - //var valueType = model.FindEntityType(typeof(ValueComplexProperties)); - //var tupleProperty = valueType.FindComplexProperty(nameof(ValueComplexProperties.Tuple)); - //Assert.False(tupleProperty.IsNullable); - //Assert.Equal(typeof((string, int)), tupleProperty.ClrType); - //var tupleType = tupleProperty.ComplexType; - //Assert.Equal(typeof((string, int)), tupleType.ClrType); - //Assert.Equal("ValueComplexProperties.Tuple#ValueTuple", tupleType.DisplayName()); + var valueType = model.FindEntityType(typeof(ValueComplexProperties))!; + var tupleProperty = valueType.FindComplexProperty(nameof(ValueComplexProperties.Tuple))!; + Assert.False(tupleProperty.IsNullable); + Assert.Equal(typeof((string, int)), tupleProperty.ClrType); + var tupleType = tupleProperty.ComplexType; + Assert.Equal(typeof((string, int)), tupleType.ClrType); + Assert.Equal("ValueComplexProperties.Tuple#ValueTuple", tupleType.DisplayName()); - //Assert.Equal(2, tupleType.GetProperties().Count()); + Assert.Equal(2, tupleType.GetProperties().Count()); } [ConditionalFact] diff --git a/test/EFCore.Tests/ModelBuilding/TestModel.cs b/test/EFCore.Tests/ModelBuilding/TestModel.cs index 1773bf787f5..037a7e5f685 100644 --- a/test/EFCore.Tests/ModelBuilding/TestModel.cs +++ b/test/EFCore.Tests/ModelBuilding/TestModel.cs @@ -917,7 +917,7 @@ protected class ValueComplexProperties { public int Id { get; set; } public ProductLabel Label { get; set; } - public ProductLabel? OldLabel { get; set; } + public ProductLabel OldLabel { get; set; } public (string, int) Tuple { get; set; } } @@ -926,7 +926,9 @@ protected struct ProductLabel public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } - public Customer? Customer { get; set; } + public Customer Customer { get; set; } + + [NotMapped] public ValueComplexProperties Parent { get; set; } } From 280a22f5a2cd0e4989cf7d2218217516a5f6fdb3 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sun, 13 Aug 2023 12:34:03 +0100 Subject: [PATCH 3/4] Allow reading complex type property value directly from the complex type --- .../CosmosSqlTranslatingExpressionVisitor.cs | 8 +-- ...yExpressionTranslatingExpressionVisitor.cs | 8 +-- ...lationalSqlTranslatingExpressionVisitor.cs | 8 +-- .../Internal/SnapshotFactoryFactory.cs | 2 +- src/EFCore/Metadata/IClrPropertyGetter.cs | 16 ++++- .../Metadata/Internal/ClrAccessorFactory.cs | 4 +- .../Metadata/Internal/ClrPropertyGetter.cs | 32 +++++++++- .../Internal/ClrPropertyGetterFactory.cs | 58 +++++++++++-------- .../Internal/ClrPropertySetterFactory.cs | 5 +- .../Internal/PropertyAccessorsFactory.cs | 2 +- src/EFCore/Metadata/Internal/PropertyBase.cs | 35 ++++++----- src/EFCore/Metadata/RuntimePropertyBase.cs | 15 ++--- .../Internal/ClrPropertyGetterFactoryTest.cs | 31 ++++++++++ 13 files changed, 159 insertions(+), 65 deletions(-) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index e9457185276..6f6b2f6beca 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -840,7 +840,7 @@ private bool TryRewriteContainsEntity(Expression source, Expression item, out Ex var propertyGetter = property.GetGetter(); foreach (var value in values) { - propertyValueList.Add(propertyGetter.GetClrValue(value)); + propertyValueList.Add(propertyGetter.GetStructuralTypeClrValue(value)); } rewrittenSource = Expression.Constant(propertyValueList); @@ -971,7 +971,7 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p { case SqlConstantExpression sqlConstantExpression: return Expression.Constant( - property.GetGetter().GetClrValue(sqlConstantExpression.Value), property.ClrType.MakeNullable()); + property.GetGetter().GetStructuralTypeClrValue(sqlConstantExpression.Value!), property.ClrType.MakeNullable()); case SqlParameterExpression sqlParameterExpression when sqlParameterExpression.Name.StartsWith(QueryCompilationContext.QueryParameterPrefix, StringComparison.Ordinal): @@ -1002,7 +1002,7 @@ when memberInitExpression.Bindings.SingleOrDefault( private static T ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { var baseParameter = context.ParameterValues[baseParameterName]; - return baseParameter == null ? (T)(object)null : (T)property.GetGetter().GetClrValue(baseParameter); + return baseParameter == null ? (T)(object)null : (T)property.GetGetter().GetStructuralTypeClrValue(baseParameter); } private static List ParameterListValueExtractor( @@ -1016,7 +1016,7 @@ private static List ParameterListValueExtractor( } var getter = property.GetGetter(); - return baseListParameter.Select(e => e != null ? (TProperty)getter.GetClrValue(e) : (TProperty)(object)null).ToList(); + return baseListParameter.Select(e => e != null ? (TProperty)getter.GetStructuralTypeClrValue(e) : (TProperty)(object)null).ToList(); } private static bool IsNullSqlConstantExpression(Expression expression) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 9b393e574c2..c39f01add88 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -1287,7 +1287,7 @@ private bool TryRewriteContainsEntity(Expression? source, Expression item, [NotN var propertyGetter = property.GetGetter(); foreach (var value in values) { - propertyValueList.Add(propertyGetter.GetClrValue(value)); + propertyValueList.Add(propertyGetter.GetStructuralTypeClrValue(value)); } rewrittenSource = Expression.Constant(propertyValueList); @@ -1435,7 +1435,7 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p return Expression.Constant( constantExpression.Value is null ? null - : property.GetGetter().GetClrValue(constantExpression.Value), + : property.GetGetter().GetStructuralTypeClrValue(constantExpression.Value), property.ClrType.MakeNullable()); case MethodCallExpression { Method.IsGenericMethod: true } methodCallExpression @@ -1478,7 +1478,7 @@ when CanEvaluate(memberInitExpression): private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { var baseParameter = context.ParameterValues[baseParameterName]; - return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); + return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetStructuralTypeClrValue(baseParameter); } private static List? ParameterListValueExtractor( @@ -1492,7 +1492,7 @@ when CanEvaluate(memberInitExpression): } var getter = property.GetGetter(); - return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetClrValue(e) : (TProperty?)(object?)null).ToList(); + return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetStructuralTypeClrValue(e) : (TProperty?)(object?)null).ToList(); } private static ConstantExpression GetValue(Expression expression) diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 668f22a9b42..5ee53fece6e 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -1619,7 +1619,7 @@ private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNu var propertyGetter = property.GetGetter(); foreach (var value in values) { - propertyValueList.Add(propertyGetter.GetClrValue(value)); + propertyValueList.Add(propertyGetter.GetStructuralTypeClrValue(value)); } rewrittenSource = Expression.Constant(propertyValueList); @@ -1815,7 +1815,7 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p return Expression.Constant( sqlConstantExpression.Value is null ? null - : property.GetGetter().GetClrValue(sqlConstantExpression.Value), + : property.GetGetter().GetStructuralTypeClrValue(sqlConstantExpression.Value), property.ClrType.MakeNullable()); case SqlParameterExpression sqlParameterExpression @@ -1847,7 +1847,7 @@ when memberInitExpression.Bindings.SingleOrDefault( private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property) { var baseParameter = context.ParameterValues[baseParameterName]; - return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter); + return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetStructuralTypeClrValue(baseParameter); } private static List? ParameterListValueExtractor( @@ -1861,7 +1861,7 @@ when memberInitExpression.Bindings.SingleOrDefault( } var getter = property.GetGetter(); - return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetClrValue(e) : (TProperty?)(object?)null).ToList(); + return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetStructuralTypeClrValue(e) : (TProperty?)(object?)null).ToList(); } private static bool CanEvaluate(Expression expression) diff --git a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs index afb30d4717c..6264efd6c36 100644 --- a/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs +++ b/src/EFCore/ChangeTracking/Internal/SnapshotFactoryFactory.cs @@ -132,7 +132,7 @@ protected virtual Expression CreateSnapshotExpression( } var memberInfo = propertyBase.GetMemberInfo(forMaterialization: false, forSet: false); - var memberAccess = PropertyBase.CreateMemberAccess(propertyBase, entityVariable!, memberInfo); + var memberAccess = PropertyBase.CreateMemberAccess(propertyBase, entityVariable!, memberInfo, fromStructuralType: false); if (memberAccess.Type != propertyBase.ClrType) { diff --git a/src/EFCore/Metadata/IClrPropertyGetter.cs b/src/EFCore/Metadata/IClrPropertyGetter.cs index cfb268e0789..a5b5652f151 100644 --- a/src/EFCore/Metadata/IClrPropertyGetter.cs +++ b/src/EFCore/Metadata/IClrPropertyGetter.cs @@ -20,7 +20,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata; public interface IClrPropertyGetter { /// - /// Gets the property value. + /// Gets the property value from the containing entity instance. /// /// The entity instance. /// The property value. @@ -32,4 +32,18 @@ public interface IClrPropertyGetter /// The entity instance. /// if the property value is the CLR default; it is any other value. bool HasSentinelValue(object entity); + + /// + /// Gets the property value from the declaring type. + /// + /// The complex type instance instance. + /// The property value. + object? GetStructuralTypeClrValue(object complexObject); + + /// + /// Checks whether or not the property is set to the CLR default for its type. + /// + /// The complex type instance instance. + /// if the property value is the CLR default; it is any other value. + bool HasStructuralTypeSentinelValue(object complexObject); } diff --git a/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs b/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs index 3d3d7c8e790..7b94b216758 100644 --- a/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrAccessorFactory.cs @@ -45,9 +45,11 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert var boundMethod = propertyBase != null ? GenericCreate.MakeGenericMethod( propertyBase.DeclaringType.ContainingEntityType.ClrType, + propertyBase.DeclaringType.ClrType, propertyBase.ClrType, propertyBase.ClrType.UnwrapNullableType()) : GenericCreate.MakeGenericMethod( + memberInfo.DeclaringType!, memberInfo.DeclaringType!, memberInfo.GetMemberType(), memberInfo.GetMemberType().UnwrapNullableType()); @@ -70,7 +72,7 @@ protected virtual TAccessor Create(MemberInfo memberInfo, IPropertyBase? propert /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected abstract TAccessor CreateGeneric( + protected abstract TAccessor CreateGeneric( MemberInfo memberInfo, IPropertyBase? propertyBase) where TEntity : class; diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs index 64709c766f7..250ebe2939c 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetter.cs @@ -12,11 +12,13 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// // Sealed for perf -public sealed class ClrPropertyGetter : IClrPropertyGetter +public sealed class ClrPropertyGetter : IClrPropertyGetter where TEntity : class { private readonly Func _getter; private readonly Func _hasSentinelValue; + private readonly Func _structuralTypeGetter; + private readonly Func _hasStructuralTypeSentinelValue; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -24,10 +26,16 @@ public sealed class ClrPropertyGetter : IClrPropertyGetter /// any release. You should only use it directly in 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 ClrPropertyGetter(Func getter, Func hasSentinelValue) + public ClrPropertyGetter( + Func getter, + Func hasSentinelValue, + Func structuralTypeGetter, + Func hasStructuralTypeSentinelValue) { _getter = getter; _hasSentinelValue = hasSentinelValue; + _structuralTypeGetter = structuralTypeGetter; + _hasStructuralTypeSentinelValue = hasStructuralTypeSentinelValue; } /// @@ -49,4 +57,24 @@ public ClrPropertyGetter(Func getter, Func hasSe [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool HasSentinelValue(object entity) => _hasSentinelValue((TEntity)entity); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in 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 object? GetStructuralTypeClrValue(object complexObject) + => _structuralTypeGetter((TStructuralType)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. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool HasStructuralTypeSentinelValue(object complexObject) + => _hasStructuralTypeSentinelValue((TStructuralType)complexObject); } diff --git a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs index 8e0eec325f4..5ceca4f98bd 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertyGetterFactory.cs @@ -28,50 +28,60 @@ public override IClrPropertyGetter Create(IPropertyBase property) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override IClrPropertyGetter CreateGeneric( + protected override IClrPropertyGetter CreateGeneric( MemberInfo memberInfo, IPropertyBase? propertyBase) { var entityClrType = propertyBase?.DeclaringType.ContainingEntityType.ClrType ?? typeof(TEntity); - var entityParameter = Expression.Parameter(entityClrType, "entity"); var propertyDeclaringType = propertyBase?.DeclaringType.ClrType ?? typeof(TEntity); + var entityParameter = Expression.Parameter(entityClrType, "entity"); + var structuralParameter = Expression.Parameter(propertyDeclaringType, "instance"); - Expression readExpression; - if (memberInfo.DeclaringType!.IsAssignableFrom(propertyDeclaringType)) - { - readExpression = PropertyBase.CreateMemberAccess(propertyBase, entityParameter, memberInfo); - } - else + var readExpression = CreateReadExpression(entityParameter, false); + var structuralReadExpression = CreateReadExpression(structuralParameter, true); + + var hasSentinelValueExpression = readExpression.MakeHasSentinelValue(propertyBase); + var hasStructuralSentinelValueExpression = structuralReadExpression.MakeHasSentinelValue(propertyBase); + + readExpression = ConvertReadExpression(readExpression, hasSentinelValueExpression); + structuralReadExpression = ConvertReadExpression(structuralReadExpression, hasStructuralSentinelValueExpression); + + return new ClrPropertyGetter( + Expression.Lambda>(readExpression, entityParameter).Compile(), + Expression.Lambda>(hasSentinelValueExpression, entityParameter).Compile(), + Expression.Lambda>(structuralReadExpression, structuralParameter).Compile(), + Expression.Lambda>(hasStructuralSentinelValueExpression, structuralParameter).Compile()); + + Expression CreateReadExpression(ParameterExpression parameter, bool fromStructuralType) { + if (memberInfo.DeclaringType!.IsAssignableFrom(propertyDeclaringType)) + { + return PropertyBase.CreateMemberAccess(propertyBase, parameter, memberInfo, fromStructuralType); + } + // This path handles properties that exist only on proxy types and so only exist if the instance is a proxy var converted = Expression.Variable(memberInfo.DeclaringType, "converted"); - readExpression = Expression.Block( + return Expression.Block( new[] { converted }, new List { Expression.Assign( converted, - Expression.TypeAs(entityParameter, memberInfo.DeclaringType)), + Expression.TypeAs(parameter, memberInfo.DeclaringType)), Expression.Condition( Expression.ReferenceEqual(converted, Expression.Constant(null)), Expression.Default(memberInfo.GetMemberType()), - PropertyBase.CreateMemberAccess(propertyBase, converted, memberInfo)) + PropertyBase.CreateMemberAccess(propertyBase, converted, memberInfo, fromStructuralType)) }); } - var hasSentinelValueExpression = readExpression.MakeHasSentinelValue(propertyBase); - - if (readExpression.Type != typeof(TValue)) - { - readExpression = Expression.Condition( - hasSentinelValueExpression, - Expression.Constant(default(TValue), typeof(TValue)), - Expression.Convert(readExpression, typeof(TValue))); - } - - return new ClrPropertyGetter( - Expression.Lambda>(readExpression, entityParameter).Compile(), - Expression.Lambda>(hasSentinelValueExpression, entityParameter).Compile()); + static Expression ConvertReadExpression(Expression expression, Expression sentinelExpression) + => expression.Type != typeof(TValue) + ? Expression.Condition( + sentinelExpression, + Expression.Constant(default(TValue), typeof(TValue)), + Expression.Convert(expression, typeof(TValue))) + : expression; } } diff --git a/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs b/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs index 0ace282c2a6..73609297cb6 100644 --- a/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs +++ b/src/EFCore/Metadata/Internal/ClrPropertySetterFactory.cs @@ -26,7 +26,7 @@ public override IClrPropertySetter Create(IPropertyBase property) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected override IClrPropertySetter CreateGeneric( + protected override IClrPropertySetter CreateGeneric( MemberInfo memberInfo, IPropertyBase? propertyBase) { @@ -82,7 +82,8 @@ Expression CreateMemberAssignment(IPropertyBase? property, Expression typeParame targetStructuralType = PropertyBase.CreateMemberAccess( complexType.ComplexProperty, typeParameter, - complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false)); + complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false), + fromStructuralType: false); } return propertyBase?.IsIndexerProperty() == true diff --git a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs index c80aaf67e52..8616a04e027 100644 --- a/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs +++ b/src/EFCore/Metadata/Internal/PropertyAccessorsFactory.cs @@ -71,7 +71,7 @@ private static Func CreateCurrentValueGetter CurrentValueComparer public static Expression CreateMemberAccess( IPropertyBase? property, Expression instanceExpression, - MemberInfo memberInfo) + MemberInfo memberInfo, + bool fromStructuralType) { if (property?.IsIndexerProperty() == true) { @@ -445,23 +446,29 @@ public static Expression CreateMemberAccess( return expression; } - if (property?.DeclaringType is IComplexType complexType) + if (!fromStructuralType + && property?.DeclaringType is IComplexType complexType) { instanceExpression = CreateMemberAccess( complexType.ComplexProperty, instanceExpression, - complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false)); - - var instanceVariable = Expression.Variable(instanceExpression.Type, "instance"); - var block = Expression.Block( - new[] { instanceVariable }, - Expression.Assign(instanceVariable, instanceExpression), - Expression.Condition( - Expression.ReferenceEqual(instanceVariable, Expression.Constant(null)), - Expression.Default(memberInfo.GetMemberType()), - Expression.MakeMemberAccess(instanceVariable, memberInfo))); - - return block; + complexType.ComplexProperty.GetMemberInfo(forMaterialization: false, forSet: false), + fromStructuralType); + + if (!instanceExpression.Type.IsValueType + || instanceExpression.Type.IsNullableValueType()) + { + var instanceVariable = Expression.Variable(instanceExpression.Type, "instance"); + var block = Expression.Block( + new[] { instanceVariable }, + Expression.Assign(instanceVariable, instanceExpression), + Expression.Condition( + Expression.Equal(instanceVariable, Expression.Constant(null)), + Expression.Default(memberInfo.GetMemberType()), + Expression.MakeMemberAccess(instanceVariable, memberInfo))); + + return block; + } } return Expression.MakeMemberAccess(instanceExpression, memberInfo); diff --git a/src/EFCore/Metadata/RuntimePropertyBase.cs b/src/EFCore/Metadata/RuntimePropertyBase.cs index c36c24cd4f0..7826ee4628e 100644 --- a/src/EFCore/Metadata/RuntimePropertyBase.cs +++ b/src/EFCore/Metadata/RuntimePropertyBase.cs @@ -150,9 +150,7 @@ public virtual void SetAccessors(PropertyAccessors accessors) [EntityFrameworkInternal] public virtual void SetSetter(Action setter) where TEntity : class - { - _setter = new ClrPropertySetter(setter); - } + => _setter = new ClrPropertySetter(setter); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -161,11 +159,14 @@ public virtual void SetSetter(Action setter) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public virtual void SetGetter(Func getter, Func hasDefaultValue) + public virtual void SetGetter( + Func getter, + Func hasDefaultValue, + Func structuralTypeGetter, + Func hasStructuralTypeSentinelValue) where TEntity : class - { - _getter = new ClrPropertyGetter(getter, hasDefaultValue); - } + => _getter = new ClrPropertyGetter( + getter, hasDefaultValue, structuralTypeGetter, hasStructuralTypeSentinelValue); /// IClrPropertySetter IRuntimePropertyBase.GetSetter() diff --git a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs index 79f741b22b6..9d273b1e15b 100644 --- a/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/ClrPropertyGetterFactoryTest.cs @@ -25,6 +25,12 @@ public object GetClrValue(object entity) public bool HasSentinelValue(object entity) => throw new NotImplementedException(); + public object GetStructuralTypeClrValue(object complexObject) + => throw new NotImplementedException(); + + public bool HasStructuralTypeSentinelValue(object complexObject) + => throw new NotImplementedException(); + public IEnumerable GetContainingForeignKeys() => throw new NotImplementedException(); @@ -181,6 +187,31 @@ public void Delegate_getter_is_returned_for_index_property() Assert.Equal(123, new ClrPropertyGetterFactory().Create((IPropertyBase)propertyB).GetClrValue(new IndexedClass { Id = 7 })); } + [ConditionalFact] + public void Delegate_getter_is_returned_for_IProperty_complex_property() + { + var modelBuilder = CreateModelBuilder(); + modelBuilder.Entity( + b => + { + b.Property(e => e.Id); + b.ComplexProperty(e => e.Fuel).Property(e => e.Volume); + }); + + var model = modelBuilder.FinalizeModel(); + + var volumeProperty = model.FindEntityType(typeof(Customer))! + .FindComplexProperty(nameof(Customer.Fuel))! + .ComplexType.FindProperty(nameof(Fuel.Volume))!; + + Assert.Equal( + 10.0, new ClrPropertyGetterFactory().Create(volumeProperty).GetClrValue( + new Customer { Id = 7, Fuel = new Fuel(10.0)})); + + Assert.Equal( + 10.0, new ClrPropertyGetterFactory().Create(volumeProperty).GetStructuralTypeClrValue(new Fuel(10.0))); + } + private static TestHelpers.TestModelBuilder CreateModelBuilder() => InMemoryTestHelpers.Instance.CreateConventionBuilder(); From 4a6a52c174fbac0a4949ed542e994005e0e2f575 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sun, 13 Aug 2023 17:39:43 +0100 Subject: [PATCH 4/4] Tweaks --- src/EFCore/Metadata/IEntityType.cs | 34 ------------------------------ src/EFCore/Metadata/ITypeBase.cs | 34 ++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index 6a02f319336..0d334942a20 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -486,40 +486,6 @@ public interface IEntityType : IReadOnlyEntityType, ITypeBase /// new IEnumerable GetDeclaredTriggers(); - /// - /// Returns all properties, including those on complex types. - /// - /// The properties. - IEnumerable GetFlattenedProperties() - { - foreach (var property in GetProperties()) - { - yield return property; - } - - foreach (var property in ReturnComplexProperties(GetComplexProperties())) - { - yield return property; - } - - IEnumerable ReturnComplexProperties(IEnumerable complexProperties) - { - foreach (var complexProperty in complexProperties) - { - var complexType = complexProperty.ComplexType; - foreach (var property in complexType.GetProperties()) - { - yield return property; - } - - foreach (var property in ReturnComplexProperties(complexType.GetComplexProperties())) - { - yield return property; - } - } - } - } - internal const DynamicallyAccessedMemberTypes DynamicallyAccessedMemberTypes = System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors diff --git a/src/EFCore/Metadata/ITypeBase.cs b/src/EFCore/Metadata/ITypeBase.cs index 51d1a37805e..fc35b15ae1f 100644 --- a/src/EFCore/Metadata/ITypeBase.cs +++ b/src/EFCore/Metadata/ITypeBase.cs @@ -199,4 +199,38 @@ public interface ITypeBase : IReadOnlyTypeBase, IAnnotatable /// /// Type members. new IEnumerable FindMembersInHierarchy(string name); + + /// + /// Returns all properties that implement , including those on complex types. + /// + /// The properties. + IEnumerable GetFlattenedProperties() + { + foreach (var property in GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(GetComplexProperties())) + { + yield return property; + } + + IEnumerable ReturnComplexProperties(IEnumerable complexProperties) + { + foreach (var complexProperty in complexProperties) + { + var complexType = complexProperty.ComplexType; + foreach (var property in complexType.GetProperties()) + { + yield return property; + } + + foreach (var property in ReturnComplexProperties(complexType.GetComplexProperties())) + { + yield return property; + } + } + } + } }