@@ -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
0 commit comments