@@ -171,6 +171,14 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171
171
/// <summary>
172
172
/// Loads the inverse relationships to prevent foreign key constraints from being violated
173
173
/// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
174
+ /// <remark>
175
+ /// Consider the following example:
176
+ /// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was
177
+ /// already related to a other person, and these persons are NOT loaded in to the
178
+ /// db context, then the query may cause a foreign key constraint. Loading
179
+ /// these "inverse relationships" into the DB context ensures EF core to take
180
+ /// this into account.
181
+ /// </remark>
174
182
/// </summary>
175
183
private void LoadInverseRelationships ( object trackedRelationshipValue , RelationshipAttribute relationshipAttr )
176
184
{
@@ -181,14 +189,15 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
181
189
if ( IsHasOneRelationship ( hasOneAttr . InverseNavigation , trackedRelationshipValue . GetType ( ) ) )
182
190
{
183
191
relationEntry . Reference ( hasOneAttr . InverseNavigation ) . Load ( ) ;
184
- } else
192
+ }
193
+ else
185
194
{
186
195
relationEntry . Collection ( hasOneAttr . InverseNavigation ) . Load ( ) ;
187
196
}
188
197
}
189
198
else if ( relationshipAttr is HasManyAttribute hasManyAttr && ! ( relationshipAttr is HasManyThroughAttribute ) )
190
199
{
191
- foreach ( IIdentifiable relationshipValue in ( IList ) trackedRelationshipValue )
200
+ foreach ( IIdentifiable relationshipValue in ( IList ) trackedRelationshipValue )
192
201
{
193
202
_context . Entry ( relationshipValue ) . Reference ( hasManyAttr . InverseNavigation ) . Load ( ) ;
194
203
}
@@ -199,13 +208,24 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
199
208
private bool IsHasOneRelationship ( string internalRelationshipName , Type type )
200
209
{
201
210
var relationshipAttr = _jsonApiContext . ResourceGraph . GetContextEntity ( type ) . Relationships . SingleOrDefault ( r => r . InternalRelationshipName == internalRelationshipName ) ;
211
+ < << << << HEAD
202
212
if ( relationshipAttr ! = null )
203
213
{
204
214
if ( relationshipAttr is HasOneAttribute ) return true ;
205
215
return false ;
206
216
} else
207
217
{
208
218
// relationshipAttr is null when we don't put a [RelationshipAttribute] on the inverse navigation property.
219
+ == = == ==
220
+ if ( relationshipAttr != null )
221
+ {
222
+ if ( relationshipAttr is HasOneAttribute ) return true;
223
+ return false;
224
+ }
225
+ else
226
+ {
227
+ // relationshipAttr is null when there is not put a [RelationshipAttribute] on the inverse navigation property.
228
+ > >>> >>> master
209
229
// In this case we use relfection to figure out what kind of relationship is pointing back.
210
230
return ! ( type . GetProperty ( internalRelationshipName ) . PropertyType . Inherits ( typeof ( IEnumerable ) ) ) ;
211
231
}
@@ -255,31 +275,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
255
275
/// <inheritdoc />
256
276
public virtual async Task < TEntity > UpdateAsync ( TEntity updatedEntity )
257
277
{
258
- var oldEntity = await GetAsync ( updatedEntity . Id ) ;
259
- if ( oldEntity == null )
278
+ var databaseEntity = await GetAsync ( updatedEntity . Id ) ;
279
+ if ( databaseEntity == null )
260
280
return null ;
261
281
262
282
foreach ( var attr in _jsonApiContext . AttributesToUpdate . Keys )
263
- attr . SetValue ( oldEntity , attr . GetValue ( updatedEntity ) ) ;
283
+ attr . SetValue ( databaseEntity , attr . GetValue ( updatedEntity ) ) ;
264
284
265
285
foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
266
286
{
267
- LoadCurrentRelationships ( oldEntity , relationshipAttr ) ;
268
- var trackedRelationshipValue = GetTrackedRelationshipValue ( relationshipAttr , updatedEntity , out bool wasAlreadyTracked ) ;
287
+ /// loads databasePerson.todoItems
288
+ LoadCurrentRelationships ( databaseEntity , relationshipAttr ) ;
289
+ /// trackedRelationshipValue is either equal to updatedPerson.todoItems
290
+ /// or replaced with the same set of todoItems from the EF Core change tracker,
291
+ /// if they were already tracked
292
+ object trackedRelationshipValue = GetTrackedRelationshipValue ( relationshipAttr , updatedEntity , out bool wasAlreadyTracked ) ;
293
+ /// loads into the db context any persons currently related
294
+ /// to the todoItems in trackedRelationshipValue
269
295
LoadInverseRelationships ( trackedRelationshipValue , relationshipAttr ) ;
270
- AssignRelationshipValue ( oldEntity , trackedRelationshipValue , relationshipAttr ) ;
296
+ /// assigns the updated relationship to the database entity
297
+ AssignRelationshipValue ( databaseEntity , trackedRelationshipValue , relationshipAttr ) ;
271
298
}
272
299
273
300
await _context . SaveChangesAsync ( ) ;
274
- return oldEntity ;
301
+ return databaseEntity ;
275
302
}
276
303
277
304
278
305
/// <summary>
279
306
/// Responsible for getting the relationship value for a given relationship
280
307
/// attribute of a given entity. It ensures that the relationship value
281
308
/// that it returns is attached to the database without reattaching duplicates instances
282
- /// to the change tracker.
309
+ /// to the change tracker. It does so by checking if there already are
310
+ /// instances of the to-be-attached entities in the change tracker.
283
311
/// </summary>
284
312
private object GetTrackedRelationshipValue ( RelationshipAttribute relationshipAttr , TEntity entity , out bool wasAlreadyAttached )
285
313
{
@@ -445,9 +473,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
445
473
446
474
/// <summary>
447
475
/// Before assigning new relationship values (UpdateAsync), we need to
448
- /// attach the current relationship state to the dbcontext, else
476
+ /// attach the current database values of the relationship to the dbcontext, else
449
477
/// it will not perform a complete-replace which is required for
450
478
/// one-to-many and many-to-many.
479
+ /// <para />
480
+ /// For example: a person `p1` has 2 todoitems: `t1` and `t2`.
481
+ /// If we want to update this todoitem set to `t3` and `t4`, simply assigning
482
+ /// `p1.todoItems = [t3, t4]` will result in EF Core adding them to the set,
483
+ /// resulting in `[t1 ... t4]`. Instead, we should first include `[t1, t2]`,
484
+ /// after which the reassignment `p1.todoItems = [t3, t4]` will actually
485
+ /// make EF Core perform a complete replace. This method does the loading of `[t1, t2]`.
451
486
/// </summary>
452
487
protected void LoadCurrentRelationships ( TEntity oldEntity , RelationshipAttribute relationshipAttribute )
453
488
{
@@ -463,21 +498,18 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
463
498
}
464
499
465
500
/// <summary>
466
- /// assigns relationships that were set in the request to the target entity of the request
467
- /// todo: partially remove dependency on IJsonApiContext here: it is fine to
468
- /// retrieve from the context WHICH relationships to update, but the actual
469
- /// values should not come from the context.
501
+ /// Assigns the <paramref name="relationshipValue"/> to <paramref name="targetEntity"/>
470
502
/// </summary>
471
- private void AssignRelationshipValue ( TEntity oldEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
503
+ private void AssignRelationshipValue ( TEntity targetEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
472
504
{
473
505
if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
474
506
{
475
507
// todo: this logic should be put in the HasManyThrough attribute
476
- AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
508
+ AssignHasManyThrough ( targetEntity , throughAttribute , ( IList ) relationshipValue ) ;
477
509
}
478
510
else
479
511
{
480
- relationshipAttribute . SetValue ( oldEntity , relationshipValue ) ;
512
+ relationshipAttribute . SetValue ( targetEntity , relationshipValue ) ;
481
513
}
482
514
}
483
515
0 commit comments