Skip to content

Commit 91580bf

Browse files
committed
fix:#502
1 parent 4a3016c commit 91580bf

File tree

3 files changed

+94
-1
lines changed

3 files changed

+94
-1
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Reflection;
56
using System.Threading.Tasks;
67
using JsonApiDotNetCore.Extensions;
78
using JsonApiDotNetCore.Internal;
@@ -10,6 +11,7 @@
1011
using JsonApiDotNetCore.Models;
1112
using JsonApiDotNetCore.Services;
1213
using Microsoft.EntityFrameworkCore;
14+
using Microsoft.EntityFrameworkCore.Metadata;
1315
using Microsoft.Extensions.Logging;
1416

1517
namespace JsonApiDotNetCore.Data
@@ -338,6 +340,15 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
338340
{
339341
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasOneAttribute)))
340342
{
343+
var attr = relationship.Key;
344+
if (_jsonApiContext.HasOneRelationshipPointers.Get().TryGetValue(attr, out var pointer) )
345+
{
346+
/// we need to attach inverse relations to make sure
347+
/// we're not violating any foreign key constraints
348+
/// when implicitely removing pre-existing relations.
349+
/// See #502 for more info.
350+
_context.LoadInverseNavigation<TEntity>(attr, pointer);
351+
}
341352
relationship.Key.SetValue(oldEntity, relationship.Value);
342353
}
343354
if ((relationship.Key.TypeId as Type).IsAssignableFrom(typeof(HasManyAttribute)))

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

+24
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,30 @@ public static class DbContextExtensions
1414
public static DbSet<T> GetDbSet<T>(this DbContext context) where T : class
1515
=> context.Set<T>();
1616

17+
public static void LoadInverseNavigation<TParent>(
18+
this DbContext context,
19+
RelationshipAttribute parentToChildAttribute,
20+
object childEntity) where TParent : class, IIdentifiable
21+
{
22+
var navigationMeta = context.Model
23+
.FindEntityType(typeof(TParent))
24+
.FindNavigation(parentToChildAttribute.InternalRelationshipName);
25+
var inverseNavigationMeta = navigationMeta.FindInverse();
26+
if (inverseNavigationMeta != null)
27+
{
28+
var inversePropertyType = inverseNavigationMeta.PropertyInfo.PropertyType;
29+
var inversePropertyName = inverseNavigationMeta.Name;
30+
var entityEntry = context.Entry(childEntity);
31+
if (inversePropertyType.IsGenericType )
32+
{ // if generic, means we're dealing with a list
33+
entityEntry.Collection(inversePropertyName).Load();
34+
} else
35+
{
36+
entityEntry.Navigation(inversePropertyName).Load();
37+
}
38+
}
39+
}
40+
1741
/// <summary>
1842
/// Get the DbSet when the model type is unknown until runtime
1943
/// </summary>

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

+59-1
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,60 @@ public async Task Updating_ToOne_Relationship_With_Implicit_Remove()
634634
context.People.AddRange(new List<Person>() { person1, person2 });
635635
await context.SaveChangesAsync();
636636

637+
var passportId = person1.PassportId;
638+
639+
var content = new
640+
{
641+
data = new
642+
{
643+
type = "people",
644+
id = person2.Id,
645+
relationships = new Dictionary<string, object>
646+
{
647+
{ "passport", new
648+
{
649+
data = new { type = "passports", id = $"{passportId}" }
650+
}
651+
}
652+
}
653+
}
654+
};
655+
656+
var httpMethod = new HttpMethod("PATCH");
657+
var route = $"/api/v1/people/{person2.Id}";
658+
var request = new HttpRequestMessage(httpMethod, route);
659+
660+
string serializedContent = JsonConvert.SerializeObject(content);
661+
request.Content = new StringContent(serializedContent);
662+
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.api+json");
663+
664+
// Act
665+
var response = await _fixture.Client.SendAsync(request);
666+
667+
// Assert
668+
var body = await response.Content.ReadAsStringAsync();
669+
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
670+
671+
var _person1 = context.People.AsNoTracking().Include(ppl => ppl.Passport).Single(ppl => ppl.Id == person1.Id);
672+
var _person2 = context.People.AsNoTracking().Include(ppl => ppl.Passport).Single(ppl => ppl.Id == person2.Id);
673+
Assert.Null(_person1.Passport);
674+
Assert.Equal(passportId, _person2.PassportId);
675+
}
676+
677+
[Fact]
678+
public async Task Updating_ToMany_Relationship_With_Implicit_Remove()
679+
{
680+
// Arrange
681+
var context = _fixture.GetService<AppDbContext>();
682+
var passport = new Passport();
683+
var person1 = _personFaker.Generate();
684+
person1.Passport = passport;
685+
var person2 = _personFaker.Generate();
686+
context.People.AddRange(new List<Person>() { person1, person2 });
687+
await context.SaveChangesAsync();
688+
689+
var passportId = person1.PassportId;
690+
637691
var content = new
638692
{
639693
data = new
@@ -644,7 +698,7 @@ public async Task Updating_ToOne_Relationship_With_Implicit_Remove()
644698
{
645699
{ "passport", new
646700
{
647-
data = new { type = "passports", id = $"{person1.PassportId}" }
701+
data = new { type = "passports", id = $"{passportId}" }
648702
}
649703
}
650704
}
@@ -666,6 +720,10 @@ public async Task Updating_ToOne_Relationship_With_Implicit_Remove()
666720
var body = await response.Content.ReadAsStringAsync();
667721
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
668722

723+
var _person1 = context.People.AsNoTracking().Include(ppl => ppl.Passport).Single(ppl => ppl.Id == person1.Id);
724+
var _person2 = context.People.AsNoTracking().Include(ppl => ppl.Passport).Single(ppl => ppl.Id == person2.Id);
725+
Assert.Null(_person1.Passport);
726+
Assert.Equal(passportId, _person2.PassportId);
669727
}
670728
}
671729
}

0 commit comments

Comments
 (0)