Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/EFCore/ChangeTracking/Internal/ChangeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ public virtual void PropertyChanged(IInternalEntry entry, IPropertyBase property

DetectKeyChange(entry, property);
}
else if (propertyBase is IComplexProperty { IsCollection: false } complexProperty)
{
// TODO: This requires notification change tracking for complex types
// Issue #36175
if (entry.EntityState is not EntityState.Deleted
&& setModified
&& entry is InternalEntryBase entryBase
&& complexProperty.IsNullable
&& complexProperty.GetOriginalValueIndex() >= 0)
{
DetectComplexPropertyChange(entryBase, complexProperty);
}
}
else if (propertyBase.GetRelationshipIndex() != -1
&& propertyBase is INavigationBase navigation)
{
Expand Down Expand Up @@ -292,11 +305,54 @@ private bool LocalDetectChanges(InternalEntryBase entry)
changesFound = true;
}
}
else if (complexProperty.IsNullable && complexProperty.GetOriginalValueIndex() >= 0)
{
if (DetectComplexPropertyChange(entry, complexProperty))
{
changesFound = true;
}
}
}

return changesFound;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComplexProperty complexProperty)
{
Check.DebugAssert(!complexProperty.IsCollection, $"Expected {complexProperty.Name} to not be a collection.");

var currentValue = entry[complexProperty];
var originalValue = entry.GetOriginalValue(complexProperty);

if ((currentValue == null) != (originalValue == null))
{
// If it changed from null to non-null, mark all inner properties as modified
// to ensure the entity is detected as modified and the complex type properties are persisted
if (currentValue != null)
{
foreach (var innerProperty in complexProperty.ComplexType.GetFlattenedProperties())
{
// Only mark properties that are tracked and can be modified
if (innerProperty.GetOriginalValueIndex() >= 0
&& innerProperty.GetAfterSaveBehavior() == PropertySaveBehavior.Save)
{
entry.SetPropertyModified(innerProperty);
}
}
}

return true;
}

return false;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ protected override Task TrackAndSaveTest<TEntity>(EntityState state, bool async,
return base.TrackAndSaveTest(state, async, createPub);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Microsoft.EntityFrameworkCore.CosmosComplexTypesTrackingTest.Throws_when_accessing_complex_entries_using_incorrect_cardinality [FAIL]
System.InvalidOperationException : An error was generated for warning 'Microsoft.EntityFrameworkCore.Model.Validation.NoPartitionKeyDefined': No partition key has been configured for entity type 'EntityWithOptionalMultiPropComplex'. It is highly recommended that an appropriate partition key be defined. See https://aka.ms/efdocs-cosmos-partition-keys for more information. This exception can be suppressed or logged by passing event ID 'CosmosEventId.NoPartitionKeyDefined' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 2db001c. Added HasPartitionKey configuration for EntityWithOptionalMultiPropComplex in the Cosmos fixture's OnModelCreating method to resolve the NoPartitionKeyDefined warning.

protected override async Task ExecuteWithStrategyInTransactionAsync(Func<DbContext, Task> testOperation, Func<DbContext, Task>? nestedTestOperation1 = null, Func<DbContext, Task>? nestedTestOperation2 = null)
protected override async Task ExecuteWithStrategyInTransactionAsync(Func<DbContext, Task> testOperation, Func<DbContext, Task>? nestedTestOperation1 = null, Func<DbContext, Task>? nestedTestOperation2 = null, Func<DbContext, Task>? nestedTestOperation3 = null)
{
using var c = CreateContext();
await c.Database.CreateExecutionStrategy().ExecuteAsync(
Expand Down Expand Up @@ -144,6 +144,16 @@ await c.Database.CreateExecutionStrategy().ExecuteAsync(
{
await nestedTestOperation2(innerContext2);
}

if (nestedTestOperation3 == null)
{
return;
}

using (var innerContext3 = CreateContext())
{
await nestedTestOperation3(innerContext3);
}
});
}

Expand All @@ -164,6 +174,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
modelBuilder.Entity<PubWithArrayCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithRecordArrayCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<PubWithPropertyBagCollections>().HasPartitionKey(x => x.Id);
modelBuilder.Entity<EntityWithOptionalMultiPropComplex>().HasPartitionKey(x => x.Id);
if (!UseProxies)
{
modelBuilder.Entity<FieldPub>().HasPartitionKey(x => x.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,24 @@ public class ComplexTypesTrackingInMemoryTest(ComplexTypesTrackingInMemoryTest.I
protected override async Task ExecuteWithStrategyInTransactionAsync(
Func<DbContext, Task> testOperation,
Func<DbContext, Task> nestedTestOperation1 = null,
Func<DbContext, Task> nestedTestOperation2 = null)
Func<DbContext, Task> nestedTestOperation2 = null,
Func<DbContext, Task> nestedTestOperation3 = null)
{
try
{
await base.ExecuteWithStrategyInTransactionAsync(testOperation, nestedTestOperation1, nestedTestOperation2);
await base.ExecuteWithStrategyInTransactionAsync(testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3);
}
finally
{
await Fixture.ReseedAsync();
}
}

public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
// InMemory provider has issues with complex type query compilation
// See https://github.com/dotnet/efcore/issues/31464
=> Task.CompletedTask;

public class InMemoryFixture : FixtureBase
{
protected override ITestStoreFactory TestStoreFactory
Expand Down
78 changes: 76 additions & 2 deletions test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2053,10 +2053,11 @@ protected static EntityEntry<TEntity> TrackFromQuery<TEntity>(DbContext context,
protected virtual Task ExecuteWithStrategyInTransactionAsync(
Func<DbContext, Task> testOperation,
Func<DbContext, Task>? nestedTestOperation1 = null,
Func<DbContext, Task>? nestedTestOperation2 = null)
Func<DbContext, Task>? nestedTestOperation2 = null,
Func<DbContext, Task>? nestedTestOperation3 = null)
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
CreateContext, UseTransaction,
testOperation, nestedTestOperation1, nestedTestOperation2);
testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3);

protected virtual void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
{
Expand Down Expand Up @@ -2397,6 +2398,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
});
});
});

modelBuilder.Entity<EntityWithOptionalMultiPropComplex>(b =>
{
b.ComplexProperty(e => e.ComplexProp);
});
}
}

Expand Down Expand Up @@ -4373,4 +4379,72 @@ protected static FieldPubWithReadonlyStructCollections CreateFieldCollectionPubW
],
FeaturedTeam = new TeamReadonlyStruct("Not In This Lifetime", ["Slash", "Axl"])
};

[ConditionalTheory(), InlineData(false), InlineData(true)]
public virtual async Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
{
await ExecuteWithStrategyInTransactionAsync(
async context =>
{
var entity = Fixture.UseProxies
? context.CreateProxy<EntityWithOptionalMultiPropComplex>()
: new EntityWithOptionalMultiPropComplex();

entity.Id = Guid.NewGuid();
entity.ComplexProp = null;

_ = async ? await context.AddAsync(entity) : context.Add(entity);
_ = async ? await context.SaveChangesAsync() : context.SaveChanges();

Assert.Null(entity.ComplexProp);
},
async context =>
{
var entity = async
? await context.Set<EntityWithOptionalMultiPropComplex>().SingleAsync()
: context.Set<EntityWithOptionalMultiPropComplex>().Single();

Assert.Null(entity.ComplexProp);

// Set the complex property with default values
entity.ComplexProp = new MultiPropComplex
{
IntValue = 0,
BoolValue = false,
DateValue = default
};

_ = async ? await context.SaveChangesAsync() : context.SaveChanges();

Assert.NotNull(entity.ComplexProp);
Assert.Equal(0, entity.ComplexProp.IntValue);
Assert.False(entity.ComplexProp.BoolValue);
Assert.Equal(default, entity.ComplexProp.DateValue);
},
async context =>
{
var entity = async
? await context.Set<EntityWithOptionalMultiPropComplex>().SingleAsync()
: context.Set<EntityWithOptionalMultiPropComplex>().Single();

// Complex types with more than one property should materialize even with default values
Assert.NotNull(entity.ComplexProp);
Assert.Equal(0, entity.ComplexProp.IntValue);
Assert.False(entity.ComplexProp.BoolValue);
Assert.Equal(default, entity.ComplexProp.DateValue);
});
}

public class EntityWithOptionalMultiPropComplex
{
public virtual Guid Id { get; set; }
public virtual MultiPropComplex? ComplexProp { get; set; }
}

public class MultiPropComplex
{
public int IntValue { get; set; }
public bool BoolValue { get; set; }
public DateTimeOffset DateValue { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,10 @@ public override void Can_write_original_values_for_properties_of_complex_propert
{
}

// Issue #36175: Complex types with notification change tracking are not supported
public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
=> Task.CompletedTask;

public class SqlServerFixture : SqlServerFixtureBase
{
protected override string StoreName
Expand Down
Loading