@@ -182,16 +182,14 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r
182182
183183 using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Repository - Create resource" ) ;
184184
185- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
186-
187185 foreach ( RelationshipAttribute relationship in _targetedFields . Relationships )
188186 {
189187 object rightValue = relationship . GetValue ( resourceFromRequest ) ;
190188
191189 object rightValueEvaluated = await VisitSetRelationshipAsync ( resourceForDatabase , relationship , rightValue , WriteOperationKind . CreateResource ,
192190 cancellationToken ) ;
193191
194- await UpdateRelationshipAsync ( relationship , resourceForDatabase , rightValueEvaluated , collector , cancellationToken ) ;
192+ await UpdateRelationshipAsync ( relationship , resourceForDatabase , rightValueEvaluated , cancellationToken ) ;
195193 }
196194
197195 foreach ( AttrAttribute attribute in _targetedFields . Attributes )
@@ -207,6 +205,8 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r
207205 await SaveChangesAsync ( cancellationToken ) ;
208206
209207 await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resourceForDatabase , WriteOperationKind . CreateResource , cancellationToken ) ;
208+
209+ _dbContext . ResetChangeTracker ( ) ;
210210 }
211211
212212 private async Task < object > VisitSetRelationshipAsync ( TResource leftResource , RelationshipAttribute relationship , object rightValue ,
@@ -254,8 +254,6 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
254254
255255 using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Repository - Update resource" ) ;
256256
257- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
258-
259257 foreach ( RelationshipAttribute relationship in _targetedFields . Relationships )
260258 {
261259 object rightValue = relationship . GetValue ( resourceFromRequest ) ;
@@ -265,7 +263,7 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
265263
266264 AssertIsNotClearingRequiredRelationship ( relationship , resourceFromDatabase , rightValueEvaluated ) ;
267265
268- await UpdateRelationshipAsync ( relationship , resourceFromDatabase , rightValueEvaluated , collector , cancellationToken ) ;
266+ await UpdateRelationshipAsync ( relationship , resourceFromDatabase , rightValueEvaluated , cancellationToken ) ;
269267 }
270268
271269 foreach ( AttrAttribute attribute in _targetedFields . Attributes )
@@ -278,18 +276,21 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
278276 await SaveChangesAsync ( cancellationToken ) ;
279277
280278 await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resourceFromDatabase , WriteOperationKind . UpdateResource , cancellationToken ) ;
279+
280+ _dbContext . ResetChangeTracker ( ) ;
281281 }
282282
283283 protected void AssertIsNotClearingRequiredRelationship ( RelationshipAttribute relationship , TResource leftResource , object rightValue )
284284 {
285- bool relationshipIsRequired = false ;
286-
287- if ( relationship is not HasManyAttribute { IsManyToMany : true } )
285+ if ( relationship is HasManyAttribute { IsManyToMany : true } )
288286 {
289- INavigation navigation = TryGetNavigation ( relationship ) ;
290- relationshipIsRequired = navigation ? . ForeignKey ? . IsRequired ?? false ;
287+ // Many-to-many relationships cannot be required.
288+ return ;
291289 }
292290
291+ INavigation navigation = TryGetNavigation ( relationship ) ;
292+ bool relationshipIsRequired = navigation ? . ForeignKey ? . IsRequired ?? false ;
293+
293294 bool relationshipIsBeingCleared = relationship is HasManyAttribute hasManyRelationship
294295 ? IsToManyRelationshipBeingCleared ( hasManyRelationship , leftResource , rightValue )
295296 : rightValue == null ;
@@ -325,32 +326,29 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
325326
326327 using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Repository - Delete resource" ) ;
327328
328- // This enables OnWritingAsync() to fetch the resource, which adds it to the change tracker.
329- // If so, we'll reuse the tracked resource instead of a placeholder resource.
330- var emptyResource = _resourceFactory . CreateInstance < TResource > ( ) ;
331- emptyResource . Id = id ;
329+ var placeholderResource = _resourceFactory . CreateInstance < TResource > ( ) ;
330+ placeholderResource . Id = id ;
332331
333- await _resourceDefinitionAccessor . OnWritingAsync ( emptyResource , WriteOperationKind . DeleteResource , cancellationToken ) ;
332+ await _resourceDefinitionAccessor . OnWritingAsync ( placeholderResource , WriteOperationKind . DeleteResource , cancellationToken ) ;
334333
335- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
336- TResource resource = collector . CreateForId < TResource , TId > ( id ) ;
334+ var resourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( placeholderResource ) ;
337335
338336 foreach ( RelationshipAttribute relationship in _resourceGraph . GetResourceContext < TResource > ( ) . Relationships )
339337 {
340338 // Loads the data of the relationship, if in EF Core it is configured in such a way that loading the related
341339 // entities into memory is required for successfully executing the selected deletion behavior.
342340 if ( RequiresLoadOfRelationshipForDeletion ( relationship ) )
343341 {
344- NavigationEntry navigation = GetNavigationEntry ( resource , relationship ) ;
342+ NavigationEntry navigation = GetNavigationEntry ( resourceTracked , relationship ) ;
345343 await navigation . LoadAsync ( cancellationToken ) ;
346344 }
347345 }
348346
349- _dbContext . Remove ( resource ) ;
347+ _dbContext . Remove ( resourceTracked ) ;
350348
351349 await SaveChangesAsync ( cancellationToken ) ;
352350
353- await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resource , WriteOperationKind . DeleteResource , cancellationToken ) ;
351+ await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resourceTracked , WriteOperationKind . DeleteResource , cancellationToken ) ;
354352 }
355353
356354 private NavigationEntry GetNavigationEntry ( TResource resource , RelationshipAttribute relationship )
@@ -413,8 +411,7 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object ri
413411
414412 AssertIsNotClearingRequiredRelationship ( relationship , leftResource , rightValueEvaluated ) ;
415413
416- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
417- await UpdateRelationshipAsync ( relationship , leftResource , rightValueEvaluated , collector , cancellationToken ) ;
414+ await UpdateRelationshipAsync ( relationship , leftResource , rightValueEvaluated , cancellationToken ) ;
418415
419416 await _resourceDefinitionAccessor . OnWritingAsync ( leftResource , WriteOperationKind . SetRelationship , cancellationToken ) ;
420417
@@ -442,16 +439,18 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentif
442439
443440 if ( rightResourceIds . Any ( ) )
444441 {
445- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
446- TResource leftResource = collector . CreateForId < TResource , TId > ( leftId ) ;
442+ var leftPlaceholderResource = _resourceFactory . CreateInstance < TResource > ( ) ;
443+ leftPlaceholderResource . Id = leftId ;
447444
448- await UpdateRelationshipAsync ( relationship , leftResource , rightResourceIds , collector , cancellationToken ) ;
445+ var leftResourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( leftPlaceholderResource ) ;
449446
450- await _resourceDefinitionAccessor . OnWritingAsync ( leftResource , WriteOperationKind . AddToRelationship , cancellationToken ) ;
447+ await UpdateRelationshipAsync ( relationship , leftResourceTracked , rightResourceIds , cancellationToken ) ;
448+
449+ await _resourceDefinitionAccessor . OnWritingAsync ( leftResourceTracked , WriteOperationKind . AddToRelationship , cancellationToken ) ;
451450
452451 await SaveChangesAsync ( cancellationToken ) ;
453452
454- await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResource , WriteOperationKind . AddToRelationship , cancellationToken ) ;
453+ await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResourceTracked , WriteOperationKind . AddToRelationship , cancellationToken ) ;
455454 }
456455 }
457456
@@ -470,35 +469,62 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
470469 using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Repository - Remove from to-many relationship" ) ;
471470
472471 var relationship = ( HasManyAttribute ) _targetedFields . Relationships . Single ( ) ;
472+ HashSet < IIdentifiable > rightResourceIdsToRemove = rightResourceIds . ToHashSet ( IdentifiableComparer . Instance ) ;
473473
474- await _resourceDefinitionAccessor . OnRemoveFromRelationshipAsync ( leftResource , relationship , rightResourceIds , cancellationToken ) ;
474+ await _resourceDefinitionAccessor . OnRemoveFromRelationshipAsync ( leftResource , relationship , rightResourceIdsToRemove , cancellationToken ) ;
475475
476- if ( rightResourceIds . Any ( ) )
476+ if ( rightResourceIdsToRemove . Any ( ) )
477477 {
478+ var leftResourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( leftResource ) ;
479+
480+ // Make EF Core believe any additional resources added from ResourceDefinition already exist in database.
481+ IIdentifiable [ ] extraResourceIdsToRemove = rightResourceIdsToRemove . Where ( rightId => ! rightResourceIds . Contains ( rightId ) ) . ToArray ( ) ;
482+
478483 object rightValueStored = relationship . GetValue ( leftResource ) ;
479484
480- HashSet < IIdentifiable > rightResourceIdsToStore =
481- _collectionConverter . ExtractResources ( rightValueStored ) . ToHashSet ( IdentifiableComparer . Instance ) ;
485+ // @formatter:wrap_chained_method_calls chop_always
486+ // @formatter:keep_existing_linebreaks true
482487
483- rightResourceIdsToStore . ExceptWith ( rightResourceIds ) ;
488+ IIdentifiable [ ] rightResourceIdsStored = _collectionConverter
489+ . ExtractResources ( rightValueStored )
490+ . Concat ( extraResourceIdsToRemove )
491+ . Select ( rightResource => _dbContext . GetTrackedOrAttach ( rightResource ) )
492+ . ToArray ( ) ;
493+
494+ // @formatter:keep_existing_linebreaks restore
495+ // @formatter:wrap_chained_method_calls restore
496+
497+ rightValueStored = _collectionConverter . CopyToTypedCollection ( rightResourceIdsStored , relationship . Property . PropertyType ) ;
498+ relationship . SetValue ( leftResource , rightValueStored ) ;
499+
500+ MarkRelationshipAsLoaded ( leftResource , relationship ) ;
484501
485- AssertIsNotClearingRequiredRelationship ( relationship , leftResource , rightResourceIdsToStore ) ;
502+ HashSet < IIdentifiable > rightResourceIdsToStore = rightResourceIdsStored . ToHashSet ( IdentifiableComparer . Instance ) ;
503+ rightResourceIdsToStore . ExceptWith ( rightResourceIdsToRemove ) ;
486504
487- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
488- await UpdateRelationshipAsync ( relationship , leftResource , rightResourceIdsToStore , collector , cancellationToken ) ;
505+ AssertIsNotClearingRequiredRelationship ( relationship , leftResourceTracked , rightResourceIdsToStore ) ;
489506
490- await _resourceDefinitionAccessor . OnWritingAsync ( leftResource , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
507+ await UpdateRelationshipAsync ( relationship , leftResourceTracked , rightResourceIdsToStore , cancellationToken ) ;
508+
509+ await _resourceDefinitionAccessor . OnWritingAsync ( leftResourceTracked , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
491510
492511 await SaveChangesAsync ( cancellationToken ) ;
493512
494- await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResource , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
513+ await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResourceTracked , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
495514 }
496515 }
497516
517+ private void MarkRelationshipAsLoaded ( TResource leftResource , RelationshipAttribute relationship )
518+ {
519+ EntityEntry < TResource > leftEntry = _dbContext . Entry ( leftResource ) ;
520+ CollectionEntry rightCollectionEntry = leftEntry . Collection ( relationship . Property . Name ) ;
521+ rightCollectionEntry . IsLoaded = true ;
522+ }
523+
498524 protected async Task UpdateRelationshipAsync ( RelationshipAttribute relationship , TResource leftResource , object valueToAssign ,
499- PlaceholderResourceCollector collector , CancellationToken cancellationToken )
525+ CancellationToken cancellationToken )
500526 {
501- object trackedValueToAssign = EnsureRelationshipValueToAssignIsTracked ( valueToAssign , relationship . Property . PropertyType , collector ) ;
527+ object trackedValueToAssign = EnsureRelationshipValueToAssignIsTracked ( valueToAssign , relationship . Property . PropertyType ) ;
502528
503529 if ( RequireLoadOfInverseRelationship ( relationship , trackedValueToAssign ) )
504530 {
@@ -511,15 +537,15 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship,
511537 relationship . SetValue ( leftResource , trackedValueToAssign ) ;
512538 }
513539
514- private object EnsureRelationshipValueToAssignIsTracked ( object rightValue , Type relationshipPropertyType , PlaceholderResourceCollector collector )
540+ private object EnsureRelationshipValueToAssignIsTracked ( object rightValue , Type relationshipPropertyType )
515541 {
516542 if ( rightValue == null )
517543 {
518544 return null ;
519545 }
520546
521547 ICollection < IIdentifiable > rightResources = _collectionConverter . ExtractResources ( rightValue ) ;
522- IIdentifiable [ ] rightResourcesTracked = rightResources . Select ( collector . CaptureExisting ) . ToArray ( ) ;
548+ IIdentifiable [ ] rightResourcesTracked = rightResources . Select ( rightResource => _dbContext . GetTrackedOrAttach ( rightResource ) ) . ToArray ( ) ;
523549
524550 return rightValue is IEnumerable
525551 ? _collectionConverter . CopyToTypedCollection ( rightResourcesTracked , relationshipPropertyType )
@@ -551,6 +577,8 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke
551577 await _dbContext . Database . CurrentTransaction . RollbackAsync ( cancellationToken ) ;
552578 }
553579
580+ _dbContext . ResetChangeTracker ( ) ;
581+
554582 throw new DataStoreUpdateException ( exception ) ;
555583 }
556584 }
0 commit comments