@@ -2085,6 +2085,323 @@ public void Can_add_owned_dependent_with_reference_to_parent(bool useAdd, bool s
2085
2085
Assert . Equal ( 1 , dependentEntry2b . Property ( dependentEntry2b . Metadata . FindPrimaryKey ( ) . Properties [ 0 ] . Name ) . CurrentValue ) ;
2086
2086
}
2087
2087
2088
+ [ ConditionalTheory ] // Issue #17828
2089
+ [ InlineData ( false ) ]
2090
+ [ InlineData ( true ) ]
2091
+ public void DetectChanges_reparents_even_when_immediate_cascade_enabled ( bool delayCascade )
2092
+ {
2093
+ using var context = new EarlyLearningCenter ( ) ;
2094
+
2095
+ // Construct initial state
2096
+ var parent1 = new Category { Id = 1 } ;
2097
+ var parent2 = new Category { Id = 2 } ;
2098
+ var child = new Product { Id = 3 , Category = parent1 } ;
2099
+
2100
+ context . AddRange ( parent1 , parent2 , child ) ;
2101
+ context . ChangeTracker . AcceptAllChanges ( ) ;
2102
+
2103
+ Assert . Equal ( 3 , context . ChangeTracker . Entries ( ) . Count ( ) ) ;
2104
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( parent1 ) . State ) ;
2105
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( parent2 ) . State ) ;
2106
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( child ) . State ) ;
2107
+
2108
+ if ( delayCascade )
2109
+ {
2110
+ context . ChangeTracker . CascadeDeleteTiming = CascadeTiming . OnSaveChanges ;
2111
+ }
2112
+
2113
+ child . Category = parent2 ;
2114
+
2115
+ context . ChangeTracker . DetectChanges ( ) ;
2116
+
2117
+ context . Remove ( parent1 ) ;
2118
+
2119
+ Assert . Equal ( 3 , context . ChangeTracker . Entries ( ) . Count ( ) ) ;
2120
+ Assert . Equal ( EntityState . Deleted , context . Entry ( parent1 ) . State ) ;
2121
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( parent2 ) . State ) ;
2122
+ Assert . Equal ( EntityState . Modified , context . Entry ( child ) . State ) ;
2123
+ }
2124
+
2125
+ [ ConditionalTheory ] // Issue #12590
2126
+ [ InlineData ( false , false ) ]
2127
+ [ InlineData ( false , true ) ]
2128
+ [ InlineData ( true , false ) ]
2129
+ [ InlineData ( true , true ) ]
2130
+ public void Dependents_are_detached_not_deleted_when_principal_is_detached ( bool delayCascade , bool trackNewDependents )
2131
+ {
2132
+ using var context = new EarlyLearningCenter ( ) ;
2133
+
2134
+ var category = new Category
2135
+ {
2136
+ Id = 1 ,
2137
+ Products = new List < Product >
2138
+ {
2139
+ new Product { Id = 1 } ,
2140
+ new Product { Id = 2 } ,
2141
+ new Product { Id = 3 }
2142
+ }
2143
+ } ;
2144
+
2145
+ context . Attach ( category ) ;
2146
+
2147
+ var categoryEntry = context . Entry ( category ) ;
2148
+ var product0Entry = context . Entry ( category . Products [ 0 ] ) ;
2149
+ var product1Entry = context . Entry ( category . Products [ 1 ] ) ;
2150
+ var product2Entry = context . Entry ( category . Products [ 2 ] ) ;
2151
+
2152
+ Assert . Equal ( EntityState . Unchanged , categoryEntry . State ) ;
2153
+ Assert . Equal ( EntityState . Unchanged , product0Entry . State ) ;
2154
+ Assert . Equal ( EntityState . Unchanged , product1Entry . State ) ;
2155
+ Assert . Equal ( EntityState . Unchanged , product2Entry . State ) ;
2156
+
2157
+ if ( delayCascade )
2158
+ {
2159
+ context . ChangeTracker . CascadeDeleteTiming = CascadeTiming . OnSaveChanges ;
2160
+ }
2161
+
2162
+ context . Entry ( category ) . State = EntityState . Detached ;
2163
+
2164
+ Assert . Equal ( EntityState . Detached , categoryEntry . State ) ;
2165
+
2166
+ if ( delayCascade )
2167
+ {
2168
+ Assert . Equal ( EntityState . Unchanged , product0Entry . State ) ;
2169
+ Assert . Equal ( EntityState . Unchanged , product1Entry . State ) ;
2170
+ Assert . Equal ( EntityState . Unchanged , product2Entry . State ) ;
2171
+ }
2172
+ else
2173
+ {
2174
+ Assert . Equal ( EntityState . Detached , product0Entry . State ) ;
2175
+ Assert . Equal ( EntityState . Detached , product1Entry . State ) ;
2176
+ Assert . Equal ( EntityState . Detached , product2Entry . State ) ;
2177
+ }
2178
+
2179
+ var newCategory = new Category { Id = 1 , } ;
2180
+
2181
+ if ( trackNewDependents )
2182
+ {
2183
+ newCategory . Products = new List < Product >
2184
+ {
2185
+ new Product { Id = 1 } ,
2186
+ new Product { Id = 2 } ,
2187
+ new Product { Id = 3 }
2188
+ } ;
2189
+ }
2190
+
2191
+ var traversal = new List < string > ( ) ;
2192
+
2193
+ if ( delayCascade && trackNewDependents )
2194
+ {
2195
+ Assert . Equal (
2196
+ CoreStrings . IdentityConflict ( nameof ( Product ) , "{'Id'}" ) ,
2197
+ Assert . Throws < InvalidOperationException > ( TrackGraph ) . Message ) ;
2198
+ }
2199
+ else
2200
+ {
2201
+ TrackGraph ( ) ;
2202
+
2203
+ Assert . Equal (
2204
+ trackNewDependents
2205
+ ? new List < string >
2206
+ {
2207
+ "<None> -----> Category:1" ,
2208
+ "Category:1 ---Products--> Product:1" ,
2209
+ "Category:1 ---Products--> Product:2" ,
2210
+ "Category:1 ---Products--> Product:3"
2211
+ }
2212
+ : new List < string >
2213
+ {
2214
+ "<None> -----> Category:1"
2215
+ } ,
2216
+ traversal ) ;
2217
+
2218
+ if ( trackNewDependents || delayCascade )
2219
+ {
2220
+ Assert . Equal ( 4 , context . ChangeTracker . Entries ( ) . Count ( ) ) ;
2221
+
2222
+ categoryEntry = context . Entry ( newCategory ) ;
2223
+ product0Entry = context . Entry ( newCategory . Products [ 0 ] ) ;
2224
+ product1Entry = context . Entry ( newCategory . Products [ 1 ] ) ;
2225
+ product2Entry = context . Entry ( newCategory . Products [ 2 ] ) ;
2226
+
2227
+ Assert . Equal ( EntityState . Modified , categoryEntry . State ) ;
2228
+
2229
+ if ( trackNewDependents )
2230
+ {
2231
+ Assert . Equal ( EntityState . Modified , product0Entry . State ) ;
2232
+ Assert . Equal ( EntityState . Modified , product1Entry . State ) ;
2233
+ Assert . Equal ( EntityState . Modified , product2Entry . State ) ;
2234
+
2235
+ Assert . NotSame ( newCategory . Products [ 0 ] , category . Products [ 0 ] ) ;
2236
+ Assert . NotSame ( newCategory . Products [ 1 ] , category . Products [ 1 ] ) ;
2237
+ Assert . NotSame ( newCategory . Products [ 2 ] , category . Products [ 2 ] ) ;
2238
+ }
2239
+ else
2240
+ {
2241
+ Assert . Equal ( EntityState . Unchanged , product0Entry . State ) ;
2242
+ Assert . Equal ( EntityState . Unchanged , product1Entry . State ) ;
2243
+ Assert . Equal ( EntityState . Unchanged , product2Entry . State ) ;
2244
+
2245
+ Assert . Same ( newCategory . Products [ 0 ] , category . Products [ 0 ] ) ;
2246
+ Assert . Same ( newCategory . Products [ 1 ] , category . Products [ 1 ] ) ;
2247
+ Assert . Same ( newCategory . Products [ 2 ] , category . Products [ 2 ] ) ;
2248
+ }
2249
+
2250
+ Assert . Same ( newCategory , newCategory . Products [ 0 ] . Category ) ;
2251
+ Assert . Same ( newCategory , newCategory . Products [ 1 ] . Category ) ;
2252
+ Assert . Same ( newCategory , newCategory . Products [ 2 ] . Category ) ;
2253
+
2254
+ Assert . Equal ( newCategory . Id , product0Entry . Property ( "CategoryId" ) . CurrentValue ) ;
2255
+ Assert . Equal ( newCategory . Id , product1Entry . Property ( "CategoryId" ) . CurrentValue ) ;
2256
+ Assert . Equal ( newCategory . Id , product2Entry . Property ( "CategoryId" ) . CurrentValue ) ;
2257
+ }
2258
+ else
2259
+ {
2260
+ Assert . Single ( context . ChangeTracker . Entries ( ) ) ;
2261
+
2262
+ categoryEntry = context . Entry ( newCategory ) ;
2263
+
2264
+ Assert . Equal ( EntityState . Modified , categoryEntry . State ) ;
2265
+ Assert . Null ( newCategory . Products ) ;
2266
+ }
2267
+ }
2268
+
2269
+ void TrackGraph ( )
2270
+ {
2271
+ context . ChangeTracker . TrackGraph (
2272
+ newCategory , n =>
2273
+ {
2274
+ n . Entry . State = EntityState . Modified ;
2275
+ traversal . Add ( NodeString ( n ) ) ;
2276
+ } ) ;
2277
+ }
2278
+ }
2279
+
2280
+ [ ConditionalTheory ] // Issue #16546
2281
+ [ InlineData ( false ) ]
2282
+ [ InlineData ( true ) ]
2283
+ public void Optional_relationship_with_cascade_still_cascades ( bool delayCascade )
2284
+ {
2285
+ Kontainer detachedContainer ;
2286
+ var databaseName = "K" + delayCascade ;
2287
+ using ( var context = new KontainerContext ( databaseName ) )
2288
+ {
2289
+ context . Add (
2290
+ new Kontainer
2291
+ {
2292
+ Name = "C1" ,
2293
+ Rooms = { new KontainerRoom { Number = 1 , Troduct = new Troduct { Description = "Heavy Engine XT3" } } }
2294
+ }
2295
+ ) ;
2296
+
2297
+ context . SaveChanges ( ) ;
2298
+
2299
+ detachedContainer = context . Set < Kontainer > ( )
2300
+ . Include ( container => container . Rooms )
2301
+ . ThenInclude ( room => room . Troduct )
2302
+ . AsNoTracking ( )
2303
+ . Single ( ) ;
2304
+ }
2305
+
2306
+ using ( var context = new KontainerContext ( databaseName ) )
2307
+ {
2308
+ var attachedContainer = context . Set < Kontainer > ( )
2309
+ . Include ( container => container . Rooms )
2310
+ . ThenInclude ( room => room . Troduct )
2311
+ . Single ( ) ;
2312
+
2313
+ Assert . Equal ( 3 , context . ChangeTracker . Entries ( ) . Count ( ) ) ;
2314
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer ) . State ) ;
2315
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer . Rooms . Single ( ) ) . State ) ;
2316
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer . Rooms . Single ( ) . Troduct ) . State ) ;
2317
+
2318
+ var detachedRoom = detachedContainer . Rooms . Single ( ) ;
2319
+ detachedRoom . Troduct = null ;
2320
+ detachedRoom . TroductId = null ;
2321
+
2322
+ var attachedRoom = attachedContainer . Rooms . Single ( ) ;
2323
+
2324
+ if ( delayCascade )
2325
+ {
2326
+ context . ChangeTracker . DeleteOrphansTiming = CascadeTiming . OnSaveChanges ;
2327
+ }
2328
+
2329
+ context . Entry ( attachedRoom ) . CurrentValues . SetValues ( detachedRoom ) ;
2330
+
2331
+ Assert . Equal ( 3 , context . ChangeTracker . Entries ( ) . Count ( ) ) ;
2332
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer ) . State ) ;
2333
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer . Rooms . Single ( ) . Troduct ) . State ) ;
2334
+
2335
+ if ( delayCascade )
2336
+ {
2337
+ Assert . Equal ( EntityState . Modified , context . Entry ( attachedContainer . Rooms . Single ( ) ) . State ) ;
2338
+ }
2339
+ else
2340
+ {
2341
+ // Deleted because FK with cascade has been set to null
2342
+ Assert . Equal ( EntityState . Deleted , context . Entry ( attachedContainer . Rooms . Single ( ) ) . State ) ;
2343
+ }
2344
+
2345
+ context . ChangeTracker . CascadeChanges ( ) ;
2346
+
2347
+ Assert . Equal ( 3 , context . ChangeTracker . Entries ( ) . Count ( ) ) ;
2348
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer ) . State ) ;
2349
+ Assert . Equal ( EntityState . Unchanged , context . Entry ( attachedContainer . Rooms . Single ( ) . Troduct ) . State ) ;
2350
+ Assert . Equal ( EntityState . Deleted , context . Entry ( attachedContainer . Rooms . Single ( ) ) . State ) ;
2351
+
2352
+ context . SaveChanges ( ) ;
2353
+ }
2354
+ }
2355
+
2356
+ private class Kontainer
2357
+ {
2358
+ public int Id { get ; set ; }
2359
+ public string Name { get ; set ; }
2360
+ public List < KontainerRoom > Rooms { get ; set ; } = new List < KontainerRoom > ( ) ;
2361
+ }
2362
+
2363
+ private class KontainerRoom
2364
+ {
2365
+ public int Id { get ; set ; }
2366
+ public int Number { get ; set ; }
2367
+ public int KontainerId { get ; set ; }
2368
+ public Kontainer Kontainer { get ; set ; }
2369
+ public int ? TroductId { get ; set ; }
2370
+ public Troduct Troduct { get ; set ; }
2371
+ }
2372
+
2373
+ private class Troduct
2374
+ {
2375
+ public int Id { get ; set ; }
2376
+ public string Description { get ; set ; }
2377
+ public List < KontainerRoom > Rooms { get ; set ; } = new List < KontainerRoom > ( ) ;
2378
+ }
2379
+
2380
+ private class KontainerContext : DbContext
2381
+ {
2382
+ private readonly string _databaseName ;
2383
+
2384
+ public KontainerContext ( string databaseName )
2385
+ {
2386
+ _databaseName = databaseName ;
2387
+ }
2388
+
2389
+ protected internal override void OnModelCreating ( ModelBuilder modelBuilder )
2390
+ {
2391
+ modelBuilder . Entity < KontainerRoom > ( )
2392
+ . HasOne ( room => room . Troduct )
2393
+ . WithMany ( product => product . Rooms )
2394
+ . HasForeignKey ( room => room . TroductId )
2395
+ . IsRequired ( false )
2396
+ . OnDelete ( DeleteBehavior . Cascade ) ;
2397
+ }
2398
+
2399
+ protected internal override void OnConfiguring ( DbContextOptionsBuilder optionsBuilder )
2400
+ => optionsBuilder
2401
+ . UseInternalServiceProvider ( InMemoryFixture . DefaultServiceProvider )
2402
+ . UseInMemoryDatabase ( _databaseName ) ;
2403
+ }
2404
+
2088
2405
[ ConditionalTheory ]
2089
2406
[ InlineData ( false ) ]
2090
2407
[ InlineData ( true ) ]
0 commit comments