@@ -171,6 +171,14 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171171 /// <summary>
172172 /// Loads the inverse relationships to prevent foreign key constraints from being violated
173173 /// 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>
174182 /// </summary>
175183 private void LoadInverseRelationships ( object trackedRelationshipValue , RelationshipAttribute relationshipAttr )
176184 {
@@ -181,14 +189,15 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
181189 if ( IsHasOneRelationship ( hasOneAttr . InverseNavigation , trackedRelationshipValue . GetType ( ) ) )
182190 {
183191 relationEntry . Reference ( hasOneAttr . InverseNavigation ) . Load ( ) ;
184- } else
192+ }
193+ else
185194 {
186195 relationEntry . Collection ( hasOneAttr . InverseNavigation ) . Load ( ) ;
187196 }
188197 }
189198 else if ( relationshipAttr is HasManyAttribute hasManyAttr && ! ( relationshipAttr is HasManyThroughAttribute ) )
190199 {
191- foreach ( IIdentifiable relationshipValue in ( IList ) trackedRelationshipValue )
200+ foreach ( IIdentifiable relationshipValue in ( IList ) trackedRelationshipValue )
192201 {
193202 _context . Entry ( relationshipValue ) . Reference ( hasManyAttr . InverseNavigation ) . Load ( ) ;
194203 }
@@ -199,13 +208,24 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
199208 private bool IsHasOneRelationship ( string internalRelationshipName , Type type )
200209 {
201210 var relationshipAttr = _jsonApiContext . ResourceGraph . GetContextEntity ( type ) . Relationships . SingleOrDefault ( r => r . InternalRelationshipName == internalRelationshipName ) ;
211+ < << << << HEAD
202212 if ( relationshipAttr ! = null )
203213 {
204214 if ( relationshipAttr is HasOneAttribute ) return true ;
205215 return false ;
206216 } else
207217 {
208218 // 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
209229 // In this case we use relfection to figure out what kind of relationship is pointing back.
210230 return ! ( type . GetProperty ( internalRelationshipName ) . PropertyType . Inherits ( typeof ( IEnumerable ) ) ) ;
211231 }
@@ -255,31 +275,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
255275 /// <inheritdoc />
256276 public virtual async Task < TEntity > UpdateAsync ( TEntity updatedEntity )
257277 {
258- var oldEntity = await GetAsync ( updatedEntity . Id ) ;
259- if ( oldEntity == null )
278+ var databaseEntity = await GetAsync ( updatedEntity . Id ) ;
279+ if ( databaseEntity == null )
260280 return null ;
261281
262282 foreach ( var attr in _jsonApiContext . AttributesToUpdate . Keys )
263- attr . SetValue ( oldEntity , attr . GetValue ( updatedEntity ) ) ;
283+ attr . SetValue ( databaseEntity , attr . GetValue ( updatedEntity ) ) ;
264284
265285 foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
266286 {
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
269295 LoadInverseRelationships ( trackedRelationshipValue , relationshipAttr ) ;
270- AssignRelationshipValue ( oldEntity , trackedRelationshipValue , relationshipAttr ) ;
296+ /// assigns the updated relationship to the database entity
297+ AssignRelationshipValue ( databaseEntity , trackedRelationshipValue , relationshipAttr ) ;
271298 }
272299
273300 await _context . SaveChangesAsync ( ) ;
274- return oldEntity ;
301+ return databaseEntity ;
275302 }
276303
277304
278305 /// <summary>
279306 /// Responsible for getting the relationship value for a given relationship
280307 /// attribute of a given entity. It ensures that the relationship value
281308 /// 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.
283311 /// </summary>
284312 private object GetTrackedRelationshipValue ( RelationshipAttribute relationshipAttr , TEntity entity , out bool wasAlreadyAttached )
285313 {
@@ -445,9 +473,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
445473
446474 /// <summary>
447475 /// 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
449477 /// it will not perform a complete-replace which is required for
450478 /// 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]`.
451486 /// </summary>
452487 protected void LoadCurrentRelationships ( TEntity oldEntity , RelationshipAttribute relationshipAttribute )
453488 {
@@ -463,21 +498,18 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
463498 }
464499
465500 /// <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"/>
470502 /// </summary>
471- private void AssignRelationshipValue ( TEntity oldEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
503+ private void AssignRelationshipValue ( TEntity targetEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
472504 {
473505 if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
474506 {
475507 // todo: this logic should be put in the HasManyThrough attribute
476- AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
508+ AssignHasManyThrough ( targetEntity , throughAttribute , ( IList ) relationshipValue ) ;
477509 }
478510 else
479511 {
480- relationshipAttribute . SetValue ( oldEntity , relationshipValue ) ;
512+ relationshipAttribute . SetValue ( targetEntity , relationshipValue ) ;
481513 }
482514 }
483515
0 commit comments