@@ -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 ;
@@ -326,31 +327,30 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
326327 using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Repository - Delete resource" ) ;
327328
328329 // 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 ;
330+ // If so, we'll reuse the tracked resource instead of this placeholder resource.
331+ var placeholderResource = _resourceFactory . CreateInstance < TResource > ( ) ;
332+ placeholderResource . Id = id ;
332333
333- await _resourceDefinitionAccessor . OnWritingAsync ( emptyResource , WriteOperationKind . DeleteResource , cancellationToken ) ;
334+ await _resourceDefinitionAccessor . OnWritingAsync ( placeholderResource , WriteOperationKind . DeleteResource , cancellationToken ) ;
334335
335- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
336- TResource resource = collector . CreateForId < TResource , TId > ( id ) ;
336+ var resourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( placeholderResource ) ;
337337
338338 foreach ( RelationshipAttribute relationship in _resourceGraph . GetResourceContext < TResource > ( ) . Relationships )
339339 {
340340 // Loads the data of the relationship, if in EF Core it is configured in such a way that loading the related
341341 // entities into memory is required for successfully executing the selected deletion behavior.
342342 if ( RequiresLoadOfRelationshipForDeletion ( relationship ) )
343343 {
344- NavigationEntry navigation = GetNavigationEntry ( resource , relationship ) ;
344+ NavigationEntry navigation = GetNavigationEntry ( resourceTracked , relationship ) ;
345345 await navigation . LoadAsync ( cancellationToken ) ;
346346 }
347347 }
348348
349- _dbContext . Remove ( resource ) ;
349+ _dbContext . Remove ( resourceTracked ) ;
350350
351351 await SaveChangesAsync ( cancellationToken ) ;
352352
353- await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resource , WriteOperationKind . DeleteResource , cancellationToken ) ;
353+ await _resourceDefinitionAccessor . OnWriteSucceededAsync ( resourceTracked , WriteOperationKind . DeleteResource , cancellationToken ) ;
354354 }
355355
356356 private NavigationEntry GetNavigationEntry ( TResource resource , RelationshipAttribute relationship )
@@ -413,8 +413,7 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object ri
413413
414414 AssertIsNotClearingRequiredRelationship ( relationship , leftResource , rightValueEvaluated ) ;
415415
416- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
417- await UpdateRelationshipAsync ( relationship , leftResource , rightValueEvaluated , collector , cancellationToken ) ;
416+ await UpdateRelationshipAsync ( relationship , leftResource , rightValueEvaluated , cancellationToken ) ;
418417
419418 await _resourceDefinitionAccessor . OnWritingAsync ( leftResource , WriteOperationKind . SetRelationship , cancellationToken ) ;
420419
@@ -442,16 +441,18 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet<IIdentif
442441
443442 if ( rightResourceIds . Any ( ) )
444443 {
445- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
446- TResource leftResource = collector . CreateForId < TResource , TId > ( leftId ) ;
444+ var leftPlaceholderResource = _resourceFactory . CreateInstance < TResource > ( ) ;
445+ leftPlaceholderResource . Id = leftId ;
447446
448- await UpdateRelationshipAsync ( relationship , leftResource , rightResourceIds , collector , cancellationToken ) ;
447+ var leftResourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( leftPlaceholderResource ) ;
449448
450- await _resourceDefinitionAccessor . OnWritingAsync ( leftResource , WriteOperationKind . AddToRelationship , cancellationToken ) ;
449+ await UpdateRelationshipAsync ( relationship , leftResourceTracked , rightResourceIds , cancellationToken ) ;
450+
451+ await _resourceDefinitionAccessor . OnWritingAsync ( leftResourceTracked , WriteOperationKind . AddToRelationship , cancellationToken ) ;
451452
452453 await SaveChangesAsync ( cancellationToken ) ;
453454
454- await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResource , WriteOperationKind . AddToRelationship , cancellationToken ) ;
455+ await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResourceTracked , WriteOperationKind . AddToRelationship , cancellationToken ) ;
455456 }
456457 }
457458
@@ -470,35 +471,62 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
470471 using IDisposable _ = CodeTimingSessionManager . Current . Measure ( "Repository - Remove from to-many relationship" ) ;
471472
472473 var relationship = ( HasManyAttribute ) _targetedFields . Relationships . Single ( ) ;
474+ HashSet < IIdentifiable > rightResourceIdsToRemove = rightResourceIds . ToHashSet ( IdentifiableComparer . Instance ) ;
473475
474- await _resourceDefinitionAccessor . OnRemoveFromRelationshipAsync ( leftResource , relationship , rightResourceIds , cancellationToken ) ;
476+ await _resourceDefinitionAccessor . OnRemoveFromRelationshipAsync ( leftResource , relationship , rightResourceIdsToRemove , cancellationToken ) ;
475477
476- if ( rightResourceIds . Any ( ) )
478+ if ( rightResourceIdsToRemove . Any ( ) )
477479 {
480+ var leftResourceTracked = ( TResource ) _dbContext . GetTrackedOrAttach ( leftResource ) ;
481+
482+ // Make EF Core believe any additional resources added from ResourceDefinition already exist in database.
483+ IIdentifiable [ ] extraResourceIdsToRemove = rightResourceIdsToRemove . Where ( rightId => ! rightResourceIds . Contains ( rightId ) ) . ToArray ( ) ;
484+
478485 object rightValueStored = relationship . GetValue ( leftResource ) ;
479486
480- HashSet < IIdentifiable > rightResourceIdsToStore =
481- _collectionConverter . ExtractResources ( rightValueStored ) . ToHashSet ( IdentifiableComparer . Instance ) ;
487+ // @formatter:wrap_chained_method_calls chop_always
488+ // @formatter:keep_existing_linebreaks true
482489
483- rightResourceIdsToStore . ExceptWith ( rightResourceIds ) ;
490+ IIdentifiable [ ] rightResourceIdsStored = _collectionConverter
491+ . ExtractResources ( rightValueStored )
492+ . Concat ( extraResourceIdsToRemove )
493+ . Select ( rightResource => _dbContext . GetTrackedOrAttach ( rightResource ) )
494+ . ToArray ( ) ;
495+
496+ // @formatter:keep_existing_linebreaks restore
497+ // @formatter:wrap_chained_method_calls restore
498+
499+ rightValueStored = _collectionConverter . CopyToTypedCollection ( rightResourceIdsStored , relationship . Property . PropertyType ) ;
500+ relationship . SetValue ( leftResource , rightValueStored ) ;
501+
502+ MarkRelationshipAsLoaded ( leftResource , relationship ) ;
484503
485- AssertIsNotClearingRequiredRelationship ( relationship , leftResource , rightResourceIdsToStore ) ;
504+ HashSet < IIdentifiable > rightResourceIdsToStore = rightResourceIdsStored . ToHashSet ( IdentifiableComparer . Instance ) ;
505+ rightResourceIdsToStore . ExceptWith ( rightResourceIdsToRemove ) ;
486506
487- using var collector = new PlaceholderResourceCollector ( _resourceFactory , _dbContext ) ;
488- await UpdateRelationshipAsync ( relationship , leftResource , rightResourceIdsToStore , collector , cancellationToken ) ;
507+ AssertIsNotClearingRequiredRelationship ( relationship , leftResourceTracked , rightResourceIdsToStore ) ;
489508
490- await _resourceDefinitionAccessor . OnWritingAsync ( leftResource , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
509+ await UpdateRelationshipAsync ( relationship , leftResourceTracked , rightResourceIdsToStore , cancellationToken ) ;
510+
511+ await _resourceDefinitionAccessor . OnWritingAsync ( leftResourceTracked , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
491512
492513 await SaveChangesAsync ( cancellationToken ) ;
493514
494- await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResource , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
515+ await _resourceDefinitionAccessor . OnWriteSucceededAsync ( leftResourceTracked , WriteOperationKind . RemoveFromRelationship , cancellationToken ) ;
495516 }
496517 }
497518
519+ private void MarkRelationshipAsLoaded ( TResource leftResource , RelationshipAttribute relationship )
520+ {
521+ EntityEntry < TResource > leftEntry = _dbContext . Entry ( leftResource ) ;
522+ CollectionEntry rightCollectionEntry = leftEntry . Collection ( relationship . Property . Name ) ;
523+ rightCollectionEntry . IsLoaded = true ;
524+ }
525+
498526 protected async Task UpdateRelationshipAsync ( RelationshipAttribute relationship , TResource leftResource , object valueToAssign ,
499- PlaceholderResourceCollector collector , CancellationToken cancellationToken )
527+ CancellationToken cancellationToken )
500528 {
501- object trackedValueToAssign = EnsureRelationshipValueToAssignIsTracked ( valueToAssign , relationship . Property . PropertyType , collector ) ;
529+ object trackedValueToAssign = EnsureRelationshipValueToAssignIsTracked ( valueToAssign , relationship . Property . PropertyType ) ;
502530
503531 if ( RequireLoadOfInverseRelationship ( relationship , trackedValueToAssign ) )
504532 {
@@ -511,15 +539,15 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship,
511539 relationship . SetValue ( leftResource , trackedValueToAssign ) ;
512540 }
513541
514- private object EnsureRelationshipValueToAssignIsTracked ( object rightValue , Type relationshipPropertyType , PlaceholderResourceCollector collector )
542+ private object EnsureRelationshipValueToAssignIsTracked ( object rightValue , Type relationshipPropertyType )
515543 {
516544 if ( rightValue == null )
517545 {
518546 return null ;
519547 }
520548
521549 ICollection < IIdentifiable > rightResources = _collectionConverter . ExtractResources ( rightValue ) ;
522- IIdentifiable [ ] rightResourcesTracked = rightResources . Select ( collector . CaptureExisting ) . ToArray ( ) ;
550+ IIdentifiable [ ] rightResourcesTracked = rightResources . Select ( rightResource => _dbContext . GetTrackedOrAttach ( rightResource ) ) . ToArray ( ) ;
523551
524552 return rightValue is IEnumerable
525553 ? _collectionConverter . CopyToTypedCollection ( rightResourcesTracked , relationshipPropertyType )
@@ -551,6 +579,8 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke
551579 await _dbContext . Database . CurrentTransaction . RollbackAsync ( cancellationToken ) ;
552580 }
553581
582+ _dbContext . ResetChangeTracker ( ) ;
583+
554584 throw new DataStoreUpdateException ( exception ) ;
555585 }
556586 }
0 commit comments