Skip to content

Commit 35a2f54

Browse files
committed
feat: implicit remove support
1 parent fbe69fc commit 35a2f54

File tree

3 files changed

+36
-17
lines changed

3 files changed

+36
-17
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,15 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
147147
foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys)
148148
{
149149
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked);
150-
// LoadInverseRelationships(trackedRelationshipValue, relationshipAttribute)
150+
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
151151
if (wasAlreadyTracked)
152152
{
153153
/// We only need to reassign the relationship value to the to-be-added
154154
/// entity when we're using a different instance (because this different one
155155
/// was already tracked) than the one assigned to the to-be-created entity.
156156
AssignRelationshipValue(entity, trackedRelationshipValue, relationshipAttr);
157-
} else if (relationshipAttr is HasManyThroughAttribute throughAttr)
157+
}
158+
else if (relationshipAttr is HasManyThroughAttribute throughAttr)
158159
{
159160
/// even if we don't have to reassign anything because of already tracked
160161
/// entities, we still need to assign the "through" entities in the case of many-to-many.
@@ -167,21 +168,41 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
167168
return entity;
168169
}
169170

171+
/// <summary>
172+
/// Loads the inverse relationships to prevent foreign key constraints from being violated
173+
/// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
174+
/// </summary>
175+
private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr)
176+
{
177+
if (relationshipAttr.InverseNavigation == null) return;
178+
if (relationshipAttr is HasOneAttribute hasOneAttr)
179+
{
180+
_context.Entry((IIdentifiable)trackedRelationshipValue).Reference(hasOneAttr.InverseNavigation).Load();
181+
}
182+
else if (relationshipAttr is HasManyAttribute hasManyAttr)
183+
{
184+
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
185+
{
186+
_context.Entry((IIdentifiable)trackedRelationshipValue).Reference(hasManyAttr.InverseNavigation).Load();
187+
}
188+
}
189+
}
190+
170191

171192
/// <inheritdoc />
172193
public void DetachRelationshipPointers(TEntity entity)
173194
{
174195

175196
foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate.Keys)
176197
{
177-
if (relationshipAttr is HasOneAttribute hasOneAttr)
198+
if (relationshipAttr is HasOneAttribute hasOneAttr)
178199
{
179200
var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttr) ?? (IIdentifiable)hasOneAttr.GetValue(entity);
180201
if (relationshipValue == null) continue;
181202
_context.Entry(relationshipValue).State = EntityState.Detached;
182203

183204
}
184-
else
205+
else
185206
{
186207
IEnumerable<IIdentifiable> relationshipValueList = (IEnumerable<IIdentifiable>)relationshipAttr.GetValue(entity);
187208
/// This adds support for resource-entity separation in the case of one-to-many.
@@ -220,7 +241,7 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
220241
{
221242
LoadCurrentRelationships(oldEntity, relationshipAttr);
222243
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
223-
// LoadInverseRelationships(trackedRelationshipValue, relationshipAttribute)
244+
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
224245
AssignRelationshipValue(oldEntity, trackedRelationshipValue, relationshipAttr);
225246
}
226247
await _context.SaveChangesAsync();
@@ -237,21 +258,21 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
237258
private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TEntity entity, out bool wasAlreadyAttached)
238259
{
239260
wasAlreadyAttached = false;
240-
if (relationshipAttr is HasOneAttribute hasOneAttribute)
261+
if (relationshipAttr is HasOneAttribute hasOneAttr)
241262
{
242263
/// This adds support for resource-entity separation in the case of one-to-one.
243-
var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttribute) ?? (IIdentifiable)hasOneAttribute.GetValue(entity);
244-
if (relationshipValue == null)
245-
return null;
246-
return GetTrackedHasOneRelationshipValue(relationshipValue, hasOneAttribute, ref wasAlreadyAttached);
264+
var relationshipValue = GetEntityResourceSeparationValue(entity, hasOneAttr) ?? (IIdentifiable)hasOneAttr.GetValue(entity);
265+
if (relationshipValue == null)
266+
return null;
267+
return GetTrackedHasOneRelationshipValue(relationshipValue, hasOneAttr, ref wasAlreadyAttached);
247268
}
248269
else
249270
{
250271
IEnumerable<IIdentifiable> relationshipValueList = (IEnumerable<IIdentifiable>)relationshipAttr.GetValue(entity);
251272
/// This adds support for resource-entity separation in the case of one-to-many.
252273
/// todo: currently there is no support for many to many relations.
253-
if (relationshipAttr is HasManyAttribute hasMany)
254-
relationshipValueList = GetEntityResourceSeparationValue(entity, hasMany) ?? relationshipValueList;
274+
if (relationshipAttr is HasManyAttribute hasMany)
275+
relationshipValueList = GetEntityResourceSeparationValue(entity, hasMany) ?? relationshipValueList;
255276
if (relationshipValueList == null) return null;
256277
return GetTrackedManyRelationshipValue(relationshipValueList, relationshipAttr, ref wasAlreadyAttached);
257278
}
@@ -279,7 +300,7 @@ private IList GetTrackedManyRelationshipValue(IEnumerable<IIdentifiable> relatio
279300
}
280301

281302
// helper method used in GetTrackedRelationshipValue. See comments there.
282-
private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationshipValue, HasOneAttribute hasOneAttribute, ref bool wasAlreadyAttached)
303+
private IIdentifiable GetTrackedHasOneRelationshipValue(IIdentifiable relationshipValue, HasOneAttribute hasOneAttr, ref bool wasAlreadyAttached)
283304
{
284305
var tracked = AttachOrGetTracked(relationshipValue);
285306
if (tracked != null) wasAlreadyAttached = true;

src/JsonApiDotNetCore/Models/HasOneAttribute.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ public HasOneAttribute(string publicName = null, Link documentLinks = Link.All,
3434
_explicitIdentifiablePropertyName = withForeignKey;
3535
InverseNavigation = inverseNavigationProperty;
3636
}
37-
public string InverseNavigation { get; internal set; }
38-
3937

4038
private readonly string _explicitIdentifiablePropertyName;
4139

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingRelationshipsTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -664,8 +664,8 @@ public async Task Updating_ToOne_Relationship_With_Implicit_Remove()
664664

665665
// Assert
666666
var body = await response.Content.ReadAsStringAsync();
667-
// this test currently fails, will be adressed in next PR
668-
//Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
667+
668+
Assert.True(HttpStatusCode.OK == response.StatusCode, $"{route} returned {response.StatusCode} status code with payload: {body}");
669669

670670
}
671671
}

0 commit comments

Comments
 (0)