Skip to content

Commit ea16aaf

Browse files
author
Bart Koelman
authored
Optimized RemoveFromToManyRelationship endpoint (#1039)
1 parent 2f157a5 commit ea16aaf

File tree

40 files changed

+590
-237
lines changed

40 files changed

+590
-237
lines changed

src/JsonApiDotNetCore/CollectionExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,16 @@ public static bool DictionaryEqual<TKey, TValue>(this IReadOnlyDictionary<TKey,
7070

7171
return true;
7272
}
73+
74+
public static void AddRange<T>(this ICollection<T> source, IEnumerable<T> itemsToAdd)
75+
{
76+
ArgumentGuard.NotNull(source, nameof(source));
77+
ArgumentGuard.NotNull(itemsToAdd, nameof(itemsToAdd));
78+
79+
foreach (T item in itemsToAdd)
80+
{
81+
source.Add(item);
82+
}
83+
}
7384
}
7485
}

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,20 @@ private void AddResourcesFromDbContext(DbContext dbContext, ResourceGraphBuilder
284284
{
285285
foreach (IEntityType entityType in dbContext.Model.GetEntityTypes())
286286
{
287-
#pragma warning disable EF1001 // Internal EF Core API usage.
288-
if (entityType is not EntityType { IsImplicitlyCreatedJoinEntityType: true })
289-
#pragma warning restore EF1001 // Internal EF Core API usage.
287+
if (!IsImplicitManyToManyJoinEntity(entityType))
290288
{
291289
builder.Add(entityType.ClrType);
292290
}
293291
}
294292
}
295293

294+
private static bool IsImplicitManyToManyJoinEntity(IEntityType entityType)
295+
{
296+
#pragma warning disable EF1001 // Internal EF Core API usage.
297+
return entityType is EntityType { IsImplicitlyCreatedJoinEntityType: true };
298+
#pragma warning restore EF1001 // Internal EF Core API usage.
299+
}
300+
296301
public void Dispose()
297302
{
298303
_intermediateProvider.Dispose();

src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/JsonApiDotNetCore/Repositories/PlaceholderResourceCollector.cs

Lines changed: 0 additions & 85 deletions
This file was deleted.

0 commit comments

Comments
 (0)