@@ -325,6 +325,9 @@ public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, C
325
325
326
326
var resourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( placeholderResource ) ;
327
327
328
+ EnsureIncomingNavigationsAreTracked ( resourceTracked ) ;
329
+
330
+ /*
328
331
foreach (RelationshipAttribute relationship in _resourceGraph.GetResourceType<TResource>().Relationships)
329
332
{
330
333
// Loads the data of the relationship, if in Entity Framework Core it is configured in such a way that loading
@@ -335,6 +338,7 @@ public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, C
335
338
await navigation.LoadAsync(cancellationToken);
336
339
}
337
340
}
341
+ */
338
342
339
343
_dbContext . Remove ( resourceTracked ) ;
340
344
@@ -343,6 +347,117 @@ public virtual async Task DeleteAsync(TResource? resourceFromDatabase, TId id, C
343
347
await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resourceTracked , WriteOperationKind . DeleteResource , cancellationToken ) ;
344
348
}
345
349
350
+ private void EnsureIncomingNavigationsAreTracked ( TResource resourceTracked )
351
+ {
352
+ IEntityType [ ] entityTypes = _dbContext . Model . GetEntityTypes ( ) . ToArray ( ) ;
353
+ IEntityType thisEntityType = entityTypes . Single ( entityType => entityType . ClrType == typeof ( TResource ) ) ;
354
+
355
+ HashSet < INavigation > navigationsToLoad = new ( ) ;
356
+
357
+ foreach ( INavigation navigation in entityTypes . SelectMany ( entityType => entityType . GetNavigations ( ) ) )
358
+ {
359
+ bool isIncomingNavigation =
360
+ navigation . IsOnDependent ? navigation . TargetEntityType == thisEntityType : navigation . DeclaringEntityType == thisEntityType ;
361
+
362
+ if ( isIncomingNavigation && navigation . ForeignKey . DeleteBehavior == DeleteBehavior . ClientSetNull )
363
+ {
364
+ navigationsToLoad . Add ( navigation ) ;
365
+ }
366
+ }
367
+
368
+ // {Navigation: Customer.FirstOrder (Order) ToPrincipal Order}
369
+ // var query = from _dbContext.Set<Customer>().Where(customer => customer.FirstOrder == resourceTracked) // .Select(customer => customer.Id)
370
+
371
+ // {Navigation: Customer.LastOrder (Order) ToPrincipal Order}
372
+ // var query = from _dbContext.Set<Customer>().Where(customer => customer.LastOrder == resourceTracked) // .Select(customer => customer.Id)
373
+
374
+ // {Navigation: Order.Parent (Order) ToPrincipal Order}
375
+ // var query = from _dbContext.Set<Order>().Where(order => order.Parent == resourceTracked) // .Select(order => order.Id)
376
+
377
+ // {Navigation: ShoppingBasket.CurrentOrder (Order) ToPrincipal Order}
378
+ // var query = from _dbContext.Set<ShoppingBasket>().Where(shoppingBasket => shoppingBasket.CurrentOrder == resourceTracked) // .Select(shoppingBasket => shoppingBasket.Id)
379
+
380
+ var nameFactory = new LambdaParameterNameFactory ( ) ;
381
+ var scopeFactory = new LambdaScopeFactory ( nameFactory ) ;
382
+
383
+ foreach ( INavigation navigation in navigationsToLoad )
384
+ {
385
+ if ( ! navigation . IsOnDependent && navigation . Inverse != null )
386
+ {
387
+ // TODO: Handle the case where there is no inverse.
388
+ continue ;
389
+ }
390
+
391
+ IQueryable source = _dbContext . Set ( navigation . DeclaringEntityType . ClrType ) ;
392
+
393
+ using LambdaScope scope = scopeFactory . CreateScope ( source . ElementType ) ;
394
+
395
+ Expression expression ;
396
+
397
+ if ( navigation . IsCollection )
398
+ {
399
+ /*
400
+ {Navigation: WorkItem.Subscribers (ISet<UserAccount>) Collection ToDependent UserAccount}
401
+
402
+ var subscribers = dbContext.WorkItems
403
+ .Where(workItem => workItem == existingWorkItem)
404
+ .Include(workItem => workItem.Subscribers)
405
+ .Select(workItem => workItem.Subscribers);
406
+ */
407
+
408
+ Expression left = scope . Accessor ;
409
+ Expression right = Expression . Constant ( resourceTracked , resourceTracked . GetType ( ) ) ;
410
+
411
+ Expression whereBody = Expression . Equal ( left , right ) ;
412
+ LambdaExpression wherePredicate = Expression . Lambda ( whereBody , scope . Parameter ) ;
413
+ Expression whereExpression = WhereExtensionMethodCall ( source . Expression , scope , wherePredicate ) ;
414
+
415
+ // TODO: Use typed overload
416
+ Expression includeExpression = IncludeExtensionMethodCall ( whereExpression , scope , navigation . Name ) ;
417
+
418
+ MemberExpression selectorBody = Expression . MakeMemberAccess ( scope . Accessor , navigation . PropertyInfo ) ;
419
+ LambdaExpression selectorLambda = Expression . Lambda ( selectorBody , scope . Parameter ) ;
420
+
421
+ expression = SelectExtensionMethodCall ( includeExpression , source . ElementType , navigation . PropertyInfo . PropertyType , selectorLambda ) ;
422
+ }
423
+ else
424
+ {
425
+ MemberExpression left = Expression . MakeMemberAccess ( scope . Parameter , navigation . PropertyInfo ) ;
426
+ ConstantExpression right = Expression . Constant ( resourceTracked , resourceTracked . GetType ( ) ) ;
427
+
428
+ Expression body = Expression . Equal ( left , right ) ;
429
+ LambdaExpression selectorLambda = Expression . Lambda ( body , scope . Parameter ) ;
430
+ expression = WhereExtensionMethodCall ( source . Expression , scope , selectorLambda ) ;
431
+ }
432
+
433
+ IQueryable queryable = source . Provider . CreateQuery ( expression ) ;
434
+
435
+ // Executes the query and loads the returned entities in the change tracker.
436
+ // We can likely optimize this by only fetching ~IDs~ (primary/foreign keys) and creating placeholder resources for them.
437
+ // The reason we can't fetch by ID is because there's no interception possible (see CompositeKeyTests); there's no access
438
+ // to QueryExpressionRewriter, and even if there was, we need to handle unexpected relationships so can't rely on our query abstractions.
439
+ object [ ] results = queryable . Cast < object > ( ) . ToArray ( ) ;
440
+ }
441
+ }
442
+
443
+ private Expression WhereExtensionMethodCall ( Expression source , LambdaScope lambdaScope , LambdaExpression predicate )
444
+ {
445
+ return Expression . Call ( typeof ( Queryable ) , "Where" , lambdaScope . Parameter . Type . AsArray ( ) , source , predicate ) ;
446
+ }
447
+
448
+ private Expression IncludeExtensionMethodCall ( Expression source , LambdaScope lambdaScope , string navigationPropertyPath )
449
+ {
450
+ Expression navigationExpression = Expression . Constant ( navigationPropertyPath ) ;
451
+
452
+ return Expression . Call ( typeof ( EntityFrameworkQueryableExtensions ) , "Include" , lambdaScope . Parameter . Type . AsArray ( ) , source , navigationExpression ) ;
453
+ }
454
+
455
+ private Expression SelectExtensionMethodCall ( Expression source , Type elementType , Type bodyType , Expression selectorBody )
456
+ {
457
+ Type [ ] typeArguments = ArrayFactory . Create ( elementType , bodyType ) ;
458
+ return Expression . Call ( typeof ( Queryable ) , "Select" , typeArguments , source , selectorBody ) ;
459
+ }
460
+
346
461
private NavigationEntry GetNavigationEntry ( TResource resource , RelationshipAttribute relationship )
347
462
{
348
463
EntityEntry < TResource > entityEntry = _dbContext . Entry ( resource ) ;
0 commit comments