@@ -62,7 +62,8 @@ public DefaultEntityRepository(
6262 _resourceDefinition = resourceDefinition ;
6363 }
6464
65- public DefaultEntityRepository (
65+ public
66+ DefaultEntityRepository (
6667 ILoggerFactory loggerFactory ,
6768 IJsonApiContext jsonApiContext ,
6869 IDbContextResolver contextResolver ,
@@ -171,6 +172,13 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171172 /// <summary>
172173 /// Loads the inverse relationships to prevent foreign key constraints from being violated
173174 /// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
175+ ///
176+ /// example:
177+ /// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was
178+ /// already related to a other person, and these persons are NOT loaded in to the
179+ /// db context, then the query may cause a foreign key constraint. Loading
180+ /// these "inverse relationships" into the DB context ensures EF core to take
181+ /// this into account.
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 }
@@ -198,9 +207,18 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
198207
199208 private bool IsHasOneRelationship ( string internalRelationshipName , Type type )
200209 {
201- var relationshipAttr = _jsonApiContext . ResourceGraph . GetContextEntity ( type ) . Relationships . Single ( r => r . InternalRelationshipName == internalRelationshipName ) ;
202- if ( relationshipAttr is HasOneAttribute ) return true ;
203- return false ;
210+ var relationshipAttr = _jsonApiContext . ResourceGraph . GetContextEntity ( type ) . Relationships . SingleOrDefault ( r => r . InternalRelationshipName == internalRelationshipName ) ;
211+ if ( relationshipAttr != null )
212+ {
213+ if ( relationshipAttr is HasOneAttribute ) return true ;
214+ return false ;
215+ }
216+ else
217+ {
218+ // relationshipAttr is null when there is not put a [RelationshipAttribute] on the inverse navigation property.
219+ // In this case we use relfection to figure out what kind of relationship is pointing back.
220+ return ! ( type . GetProperty ( internalRelationshipName ) . PropertyType . Inherits ( typeof ( IEnumerable ) ) ) ;
221+ }
204222 }
205223
206224
@@ -247,31 +265,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
247265 /// <inheritdoc />
248266 public virtual async Task < TEntity > UpdateAsync ( TEntity updatedEntity )
249267 {
250- var oldEntity = await GetAsync ( updatedEntity . Id ) ;
251- if ( oldEntity == null )
268+ var databaseEntity = await GetAsync ( updatedEntity . Id ) ;
269+ if ( databaseEntity == null )
252270 return null ;
253271
254272 foreach ( var attr in _jsonApiContext . AttributesToUpdate . Keys )
255- attr . SetValue ( oldEntity , attr . GetValue ( updatedEntity ) ) ;
273+ attr . SetValue ( databaseEntity , attr . GetValue ( updatedEntity ) ) ;
256274
257275 foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
258276 {
259- LoadCurrentRelationships ( oldEntity , relationshipAttr ) ;
260- var trackedRelationshipValue = GetTrackedRelationshipValue ( relationshipAttr , updatedEntity , out bool wasAlreadyTracked ) ;
277+ /// loads databasePerson.todoItems
278+ LoadCurrentRelationships ( databaseEntity , relationshipAttr ) ;
279+ /// trackedRelationshipValue is either equal to updatedPerson.todoItems
280+ /// or replaced with the same set of todoItems from the EF Core change tracker,
281+ /// if they were already tracked
282+ object trackedRelationshipValue = GetTrackedRelationshipValue ( relationshipAttr , updatedEntity , out bool wasAlreadyTracked ) ;
283+ /// loads into the db context any persons currently related
284+ /// to the todoItems in trackedRelationshipValue
261285 LoadInverseRelationships ( trackedRelationshipValue , relationshipAttr ) ;
262- AssignRelationshipValue ( oldEntity , trackedRelationshipValue , relationshipAttr ) ;
286+ /// assigns the updated relationship to the database entity
287+ AssignRelationshipValue ( databaseEntity , trackedRelationshipValue , relationshipAttr ) ;
263288 }
264289
265290 await _context . SaveChangesAsync ( ) ;
266- return oldEntity ;
291+ return databaseEntity ;
267292 }
268293
269294
270295 /// <summary>
271296 /// Responsible for getting the relationship value for a given relationship
272297 /// attribute of a given entity. It ensures that the relationship value
273298 /// that it returns is attached to the database without reattaching duplicates instances
274- /// to the change tracker.
299+ /// to the change tracker. It does so by checking if there already are
300+ /// instances of the to-be-attached entities in the change tracker.
275301 /// </summary>
276302 private object GetTrackedRelationshipValue ( RelationshipAttribute relationshipAttr , TEntity entity , out bool wasAlreadyAttached )
277303 {
@@ -436,9 +462,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
436462
437463 /// <summary>
438464 /// Before assigning new relationship values (UpdateAsync), we need to
439- /// attach the current relationship state to the dbcontext, else
465+ /// attach the current database values of the relationship to the dbcontext, else
440466 /// it will not perform a complete-replace which is required for
441467 /// one-to-many and many-to-many.
468+ /// <para />
469+ /// For example: a person `p1` has 2 todoitems: `t1` and `t2`.
470+ /// If we want to update this todoitem set to `t3` and `t4`, simply assigning
471+ /// `p1.todoItems = [t3, t4]` will result in EF Core adding them to the set,
472+ /// resulting in `[t1 ... t4]`. Instead, we should first include `[t1, t2]`,
473+ /// after which the reassignment `p1.todoItems = [t3, t4]` will actually
474+ /// make EF Core perform a complete replace. This method does the loading of `[t1, t2]`.
442475 /// </summary>
443476 protected void LoadCurrentRelationships ( TEntity oldEntity , RelationshipAttribute relationshipAttribute )
444477 {
@@ -454,21 +487,18 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
454487 }
455488
456489 /// <summary>
457- /// assigns relationships that were set in the request to the target entity of the request
458- /// todo: partially remove dependency on IJsonApiContext here: it is fine to
459- /// retrieve from the context WHICH relationships to update, but the actual
460- /// values should not come from the context.
490+ /// Assigns the <paramref name="relationshipValue"/> to <paramref name="targetEntity"/>
461491 /// </summary>
462- private void AssignRelationshipValue ( TEntity oldEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
492+ private void AssignRelationshipValue ( TEntity targetEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
463493 {
464494 if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
465495 {
466496 // todo: this logic should be put in the HasManyThrough attribute
467- AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
497+ AssignHasManyThrough ( targetEntity , throughAttribute , ( IList ) relationshipValue ) ;
468498 }
469499 else
470500 {
471- relationshipAttribute . SetValue ( oldEntity , relationshipValue ) ;
501+ relationshipAttribute . SetValue ( targetEntity , relationshipValue ) ;
472502 }
473503 }
474504
0 commit comments