Skip to content

Commit 9c79994

Browse files
author
Bart Koelman
committed
Fixed: do not fail when clearing a required to-many relationship
1 parent a3e087c commit 9c79994

File tree

2 files changed

+46
-59
lines changed

2 files changed

+46
-59
lines changed

src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
273273
object? rightValueEvaluated = await VisitSetRelationshipAsync(resourceFromDatabase, relationship, rightValue, WriteOperationKind.UpdateResource,
274274
cancellationToken);
275275

276-
AssertIsNotClearingRequiredRelationship(relationship, resourceFromDatabase, rightValueEvaluated);
276+
AssertIsNotClearingRequiredToOneRelationship(relationship, resourceFromDatabase, rightValueEvaluated);
277277

278278
await UpdateRelationshipAsync(relationship, resourceFromDatabase, rightValueEvaluated, cancellationToken);
279279
}
@@ -292,42 +292,23 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
292292
_dbContext.ResetChangeTracker();
293293
}
294294

295-
protected void AssertIsNotClearingRequiredRelationship(RelationshipAttribute relationship, TResource leftResource, object? rightValue)
295+
protected void AssertIsNotClearingRequiredToOneRelationship(RelationshipAttribute relationship, TResource leftResource, object? rightValue)
296296
{
297-
if (relationship is HasManyAttribute { IsManyToMany: true })
297+
if (relationship is HasOneAttribute)
298298
{
299-
// Many-to-many relationships cannot be required.
300-
return;
301-
}
302-
303-
INavigation? navigation = TryGetNavigation(relationship);
304-
bool relationshipIsRequired = navigation?.ForeignKey?.IsRequired ?? false;
299+
INavigation? navigation = TryGetNavigation(relationship);
300+
bool isRelationshipRequired = navigation?.ForeignKey?.IsRequired ?? false;
305301

306-
bool relationshipIsBeingCleared = relationship is HasManyAttribute hasManyRelationship
307-
? IsToManyRelationshipBeingCleared(hasManyRelationship, leftResource, rightValue)
308-
: rightValue == null;
302+
bool isClearingRelationship = rightValue == null;
309303

310-
if (relationshipIsRequired && relationshipIsBeingCleared)
311-
{
312-
string resourceName = _resourceGraph.GetResourceType<TResource>().PublicName;
313-
throw new CannotClearRequiredRelationshipException(relationship.PublicName, leftResource.StringId!, resourceName);
304+
if (isRelationshipRequired && isClearingRelationship)
305+
{
306+
string resourceName = _resourceGraph.GetResourceType<TResource>().PublicName;
307+
throw new CannotClearRequiredRelationshipException(relationship.PublicName, leftResource.StringId!, resourceName);
308+
}
314309
}
315310
}
316311

317-
private bool IsToManyRelationshipBeingCleared(HasManyAttribute hasManyRelationship, TResource leftResource, object? valueToAssign)
318-
{
319-
ICollection<IIdentifiable> newRightResourceIds = _collectionConverter.ExtractResources(valueToAssign);
320-
321-
object? existingRightValue = hasManyRelationship.GetValue(leftResource);
322-
323-
HashSet<IIdentifiable> existingRightResourceIds =
324-
_collectionConverter.ExtractResources(existingRightValue).ToHashSet(IdentifiableComparer.Instance);
325-
326-
existingRightResourceIds.ExceptWith(newRightResourceIds);
327-
328-
return existingRightResourceIds.Any();
329-
}
330-
331312
/// <inheritdoc />
332313
public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken)
333314
{
@@ -425,7 +406,7 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object? r
425406
object? rightValueEvaluated =
426407
await VisitSetRelationshipAsync(leftResource, relationship, rightValue, WriteOperationKind.SetRelationship, cancellationToken);
427408

428-
AssertIsNotClearingRequiredRelationship(relationship, leftResource, rightValueEvaluated);
409+
AssertIsNotClearingRequiredToOneRelationship(relationship, leftResource, rightValueEvaluated);
429410

430411
await UpdateRelationshipAsync(relationship, leftResource, rightValueEvaluated, cancellationToken);
431412

@@ -519,7 +500,7 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
519500
HashSet<IIdentifiable> rightResourceIdsToStore = rightResourceIdsStored.ToHashSet(IdentifiableComparer.Instance);
520501
rightResourceIdsToStore.ExceptWith(rightResourceIdsToRemove);
521502

522-
AssertIsNotClearingRequiredRelationship(relationship, leftResourceTracked, rightResourceIdsToStore);
503+
AssertIsNotClearingRequiredToOneRelationship(relationship, leftResourceTracked, rightResourceIdsToStore);
523504

524505
await UpdateRelationshipAsync(relationship, leftResourceTracked, rightResourceIdsToStore, cancellationToken);
525506

test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
256256
}
257257

258258
[Fact]
259-
public async Task Cannot_clear_required_OneToMany_relationship_through_primary_endpoint()
259+
public async Task Clearing_OneToMany_relationship_through_primary_endpoint_triggers_cascading_delete()
260260
{
261261
// Arrange
262262
Order existingOrder = _fakers.Orders.Generate();
@@ -288,23 +288,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
288288
string route = $"/customers/{existingOrder.Customer.StringId}";
289289

290290
// Act
291-
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync<Document>(route, requestBody);
291+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync<string>(route, requestBody);
292292

293293
// Assert
294-
httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
294+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent);
295295

296-
responseDocument.Errors.Should().HaveCount(1);
296+
responseDocument.Should().BeEmpty();
297297

298-
ErrorObject error = responseDocument.Errors[0];
299-
error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
300-
error.Title.Should().Be("Failed to clear a required relationship.");
298+
await _testContext.RunOnDatabaseAsync(async dbContext =>
299+
{
300+
Order orderInDatabase = await dbContext.Orders.Include(order => order.Customer).FirstWithIdOrDefaultAsync(existingOrder.Id);
301+
orderInDatabase.Should().BeNull();
301302

302-
error.Detail.Should().Be($"The relationship 'orders' on resource type 'customers' with ID '{existingOrder.StringId}' " +
303-
"cannot be cleared because it is a required relationship.");
303+
Customer customerInDatabase = await dbContext.Customers.Include(customer => customer.Orders).FirstWithIdAsync(existingOrder.Customer.Id);
304+
customerInDatabase.Orders.Should().BeEmpty();
305+
});
304306
}
305307

306308
[Fact]
307-
public async Task Cannot_clear_required_OneToMany_relationship_by_updating_through_relationship_endpoint()
309+
public async Task Clearing_OneToMany_relationship_through_update_relationship_endpoint_triggers_cascading_delete()
308310
{
309311
// Arrange
310312
Order existingOrder = _fakers.Orders.Generate();
@@ -325,23 +327,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
325327
string route = $"/customers/{existingOrder.Customer.StringId}/relationships/orders";
326328

327329
// Act
328-
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync<Document>(route, requestBody);
330+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync<string>(route, requestBody);
329331

330332
// Assert
331-
httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
333+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent);
332334

333-
responseDocument.Errors.Should().HaveCount(1);
335+
responseDocument.Should().BeEmpty();
334336

335-
ErrorObject error = responseDocument.Errors[0];
336-
error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
337-
error.Title.Should().Be("Failed to clear a required relationship.");
337+
await _testContext.RunOnDatabaseAsync(async dbContext =>
338+
{
339+
Order orderInDatabase = await dbContext.Orders.Include(order => order.Customer).FirstWithIdOrDefaultAsync(existingOrder.Id);
340+
orderInDatabase.Should().BeNull();
338341

339-
error.Detail.Should().Be($"The relationship 'orders' on resource type 'customers' with ID '{existingOrder.StringId}' " +
340-
"cannot be cleared because it is a required relationship.");
342+
Customer customerInDatabase = await dbContext.Customers.Include(customer => customer.Orders).FirstWithIdAsync(existingOrder.Customer.Id);
343+
customerInDatabase.Orders.Should().BeEmpty();
344+
});
341345
}
342346

343347
[Fact]
344-
public async Task Cannot_clear_required_OneToMany_relationship_by_deleting_through_relationship_endpoint()
348+
public async Task Clearing_OneToMany_relationship_through_delete_relationship_endpoint_triggers_cascading_delete()
345349
{
346350
// Arrange
347351
Order existingOrder = _fakers.Orders.Generate();
@@ -369,19 +373,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
369373
string route = $"/customers/{existingOrder.Customer.StringId}/relationships/orders";
370374

371375
// Act
372-
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync<Document>(route, requestBody);
376+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync<string>(route, requestBody);
373377

374378
// Assert
375-
httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest);
379+
httpResponse.Should().HaveStatusCode(HttpStatusCode.NoContent);
376380

377-
responseDocument.Errors.Should().HaveCount(1);
381+
responseDocument.Should().BeEmpty();
378382

379-
ErrorObject error = responseDocument.Errors[0];
380-
error.StatusCode.Should().Be(HttpStatusCode.BadRequest);
381-
error.Title.Should().Be("Failed to clear a required relationship.");
383+
await _testContext.RunOnDatabaseAsync(async dbContext =>
384+
{
385+
Order orderInDatabase = await dbContext.Orders.Include(order => order.Customer).FirstWithIdOrDefaultAsync(existingOrder.Id);
386+
orderInDatabase.Should().BeNull();
382387

383-
error.Detail.Should().Be($"The relationship 'orders' on resource type 'customers' with ID '{existingOrder.StringId}' " +
384-
"cannot be cleared because it is a required relationship.");
388+
Customer customerInDatabase = await dbContext.Customers.Include(customer => customer.Orders).FirstWithIdAsync(existingOrder.Customer.Id);
389+
customerInDatabase.Orders.Should().BeEmpty();
390+
});
385391
}
386392

387393
[Fact]

0 commit comments

Comments
 (0)