Skip to content

Commit 4772327

Browse files
CopilotAndriySvyryd
andcommitted
Apply PR #37387 changes with Issue37387 quirk mode switch
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
1 parent 9861af8 commit 4772327

4 files changed

Lines changed: 167 additions & 20 deletions

File tree

src/EFCore/ChangeTracking/Internal/ChangeDetector.cs

Lines changed: 77 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public class ChangeDetector : IChangeDetector
1818
private readonly ILoggingOptions _loggingOptions;
1919
private bool _inCascadeDelete;
2020

21+
private static readonly bool UseOldBehavior37387 =
22+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37387", out var enabled) && enabled;
23+
2124
/// <summary>
2225
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2326
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -46,24 +49,39 @@ public virtual void PropertyChanged(IInternalEntry entry, IPropertyBase property
4649
return;
4750
}
4851

49-
if (propertyBase is IProperty property)
52+
switch (propertyBase)
5053
{
51-
if (entry.EntityState is not EntityState.Deleted)
52-
{
53-
entry.SetPropertyModified(property, setModified);
54-
}
55-
else
56-
{
57-
ThrowIfKeyChanged(entry, property);
58-
}
54+
case IProperty property:
55+
if (entry.EntityState is not EntityState.Deleted)
56+
{
57+
entry.SetPropertyModified(property, setModified);
58+
}
59+
else
60+
{
61+
ThrowIfKeyChanged(entry, property);
62+
}
5963

60-
DetectKeyChange(entry, property);
61-
}
62-
else if (propertyBase.GetRelationshipIndex() != -1
63-
&& propertyBase is INavigationBase navigation)
64-
{
65-
DetectNavigationChange(
66-
entry as InternalEntityEntry ?? throw new UnreachableException("Complex type entry with a navigation"), navigation);
64+
DetectKeyChange(entry, property);
65+
break;
66+
67+
case IComplexProperty { IsCollection: false } complexProperty:
68+
// TODO: This requires notification change tracking for complex types
69+
// Issue #36175
70+
if (!UseOldBehavior37387
71+
&& entry.EntityState is not EntityState.Deleted
72+
&& setModified
73+
&& entry is InternalEntryBase entryBase
74+
&& complexProperty.IsNullable
75+
&& complexProperty.GetOriginalValueIndex() >= 0)
76+
{
77+
DetectComplexPropertyChange(entryBase, complexProperty);
78+
}
79+
break;
80+
81+
case INavigationBase navigation when propertyBase.GetRelationshipIndex() != -1:
82+
DetectNavigationChange(
83+
entry as InternalEntityEntry ?? throw new UnreachableException("Complex type entry with a navigation"), navigation);
84+
break;
6785
}
6886
}
6987

@@ -292,11 +310,54 @@ private bool LocalDetectChanges(InternalEntryBase entry)
292310
changesFound = true;
293311
}
294312
}
313+
else if (!UseOldBehavior37387 && complexProperty.IsNullable && complexProperty.GetOriginalValueIndex() >= 0)
314+
{
315+
if (DetectComplexPropertyChange(entry, complexProperty))
316+
{
317+
changesFound = true;
318+
}
319+
}
295320
}
296321

297322
return changesFound;
298323
}
299324

325+
/// <summary>
326+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
327+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
328+
/// any release. You should only use it directly in your code with extreme caution and knowing that
329+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
330+
/// </summary>
331+
public virtual bool DetectComplexPropertyChange(InternalEntryBase entry, IComplexProperty complexProperty)
332+
{
333+
Check.DebugAssert(!complexProperty.IsCollection, $"Expected {complexProperty.Name} to not be a collection.");
334+
335+
var currentValue = entry[complexProperty];
336+
var originalValue = entry.GetOriginalValue(complexProperty);
337+
338+
if ((currentValue is null) != (originalValue is null))
339+
{
340+
// If it changed from null to non-null, mark all inner properties as modified
341+
// to ensure the entity is detected as modified and the complex type properties are persisted
342+
if (currentValue is not null)
343+
{
344+
foreach (var innerProperty in complexProperty.ComplexType.GetFlattenedProperties())
345+
{
346+
// Only mark properties that are tracked and can be modified
347+
if (innerProperty.GetOriginalValueIndex() >= 0
348+
&& innerProperty.GetAfterSaveBehavior() == PropertySaveBehavior.Save)
349+
{
350+
entry.SetPropertyModified(innerProperty);
351+
}
352+
}
353+
}
354+
355+
return true;
356+
}
357+
358+
return false;
359+
}
360+
300361
/// <summary>
301362
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
302363
/// the same compatibility standards as public APIs. It may be changed or removed without notice in

test/EFCore.InMemory.FunctionalTests/ComplexTypesTrackingInMemoryTest.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,24 @@ public class ComplexTypesTrackingInMemoryTest(ComplexTypesTrackingInMemoryTest.I
99
protected override async Task ExecuteWithStrategyInTransactionAsync(
1010
Func<DbContext, Task> testOperation,
1111
Func<DbContext, Task> nestedTestOperation1 = null,
12-
Func<DbContext, Task> nestedTestOperation2 = null)
12+
Func<DbContext, Task> nestedTestOperation2 = null,
13+
Func<DbContext, Task> nestedTestOperation3 = null)
1314
{
1415
try
1516
{
16-
await base.ExecuteWithStrategyInTransactionAsync(testOperation, nestedTestOperation1, nestedTestOperation2);
17+
await base.ExecuteWithStrategyInTransactionAsync(testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3);
1718
}
1819
finally
1920
{
2021
await Fixture.ReseedAsync();
2122
}
2223
}
2324

25+
public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
26+
// InMemory provider has issues with complex type query compilation
27+
// See https://github.com/dotnet/efcore/issues/31464
28+
=> Task.CompletedTask;
29+
2430
public class InMemoryFixture : FixtureBase
2531
{
2632
protected override ITestStoreFactory TestStoreFactory

test/EFCore.Specification.Tests/ComplexTypesTrackingTestBase.cs

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,10 +2069,11 @@ protected static EntityEntry<TEntity> TrackFromQuery<TEntity>(DbContext context,
20692069
protected virtual Task ExecuteWithStrategyInTransactionAsync(
20702070
Func<DbContext, Task> testOperation,
20712071
Func<DbContext, Task>? nestedTestOperation1 = null,
2072-
Func<DbContext, Task>? nestedTestOperation2 = null)
2072+
Func<DbContext, Task>? nestedTestOperation2 = null,
2073+
Func<DbContext, Task>? nestedTestOperation3 = null)
20732074
=> TestHelpers.ExecuteWithStrategyInTransactionAsync(
20742075
CreateContext, UseTransaction,
2075-
testOperation, nestedTestOperation1, nestedTestOperation2);
2076+
testOperation, nestedTestOperation1, nestedTestOperation2, nestedTestOperation3);
20762077

20772078
protected virtual void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction)
20782079
{
@@ -2413,6 +2414,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
24132414
});
24142415
});
24152416
});
2417+
2418+
modelBuilder.Entity<EntityWithOptionalMultiPropComplex>(b =>
2419+
{
2420+
b.ComplexProperty(e => e.ComplexProp);
2421+
});
24162422
}
24172423
}
24182424

@@ -4389,4 +4395,74 @@ protected static FieldPubWithReadonlyStructCollections CreateFieldCollectionPubW
43894395
],
43904396
FeaturedTeam = new TeamReadonlyStruct("Not In This Lifetime", ["Slash", "Axl"])
43914397
};
4398+
4399+
[ConditionalTheory]
4400+
[InlineData(false)]
4401+
[InlineData(true)]
4402+
public virtual async Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
4403+
{
4404+
await ExecuteWithStrategyInTransactionAsync(
4405+
async context =>
4406+
{
4407+
var entity = Fixture.UseProxies
4408+
? context.CreateProxy<EntityWithOptionalMultiPropComplex>()
4409+
: new EntityWithOptionalMultiPropComplex();
4410+
4411+
entity.Id = Guid.NewGuid();
4412+
entity.ComplexProp = null;
4413+
4414+
_ = async ? await context.AddAsync(entity) : context.Add(entity);
4415+
_ = async ? await context.SaveChangesAsync() : context.SaveChanges();
4416+
4417+
Assert.Null(entity.ComplexProp);
4418+
},
4419+
async context =>
4420+
{
4421+
var entity = async
4422+
? await context.Set<EntityWithOptionalMultiPropComplex>().SingleAsync()
4423+
: context.Set<EntityWithOptionalMultiPropComplex>().Single();
4424+
4425+
Assert.Null(entity.ComplexProp);
4426+
4427+
// Set the complex property with default values
4428+
entity.ComplexProp = new MultiPropComplex
4429+
{
4430+
IntValue = 0,
4431+
BoolValue = false,
4432+
DateValue = default
4433+
};
4434+
4435+
_ = async ? await context.SaveChangesAsync() : context.SaveChanges();
4436+
4437+
Assert.NotNull(entity.ComplexProp);
4438+
Assert.Equal(0, entity.ComplexProp.IntValue);
4439+
Assert.False(entity.ComplexProp.BoolValue);
4440+
Assert.Equal(default, entity.ComplexProp.DateValue);
4441+
},
4442+
async context =>
4443+
{
4444+
var entity = async
4445+
? await context.Set<EntityWithOptionalMultiPropComplex>().SingleAsync()
4446+
: context.Set<EntityWithOptionalMultiPropComplex>().Single();
4447+
4448+
// Complex types with more than one property should materialize even with default values
4449+
Assert.NotNull(entity.ComplexProp);
4450+
Assert.Equal(0, entity.ComplexProp.IntValue);
4451+
Assert.False(entity.ComplexProp.BoolValue);
4452+
Assert.Equal(default, entity.ComplexProp.DateValue);
4453+
});
4454+
}
4455+
4456+
public class EntityWithOptionalMultiPropComplex
4457+
{
4458+
public virtual Guid Id { get; set; }
4459+
public virtual MultiPropComplex? ComplexProp { get; set; }
4460+
}
4461+
4462+
public class MultiPropComplex
4463+
{
4464+
public int IntValue { get; set; }
4465+
public bool BoolValue { get; set; }
4466+
public DateTimeOffset DateValue { get; set; }
4467+
}
43924468
}

test/EFCore.SqlServer.FunctionalTests/ComplexTypesTrackingSqlServerTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,10 @@ public override void Can_write_original_values_for_properties_of_complex_propert
338338
{
339339
}
340340

341+
// Issue #36175: Complex types with notification change tracking are not supported
342+
public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
343+
=> Task.CompletedTask;
344+
341345
public class SqlServerFixture : SqlServerFixtureBase
342346
{
343347
protected override string StoreName

0 commit comments

Comments
 (0)