@@ -62,7 +62,8 @@ public DefaultEntityRepository(
62
62
_resourceDefinition = resourceDefinition ;
63
63
}
64
64
65
- public DefaultEntityRepository (
65
+ public
66
+ DefaultEntityRepository (
66
67
ILoggerFactory loggerFactory ,
67
68
IJsonApiContext jsonApiContext ,
68
69
IDbContextResolver contextResolver ,
@@ -171,6 +172,13 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171
172
/// <summary>
172
173
/// Loads the inverse relationships to prevent foreign key constraints from being violated
173
174
/// 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.
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
}
@@ -198,9 +207,18 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
198
207
199
208
private bool IsHasOneRelationship ( string internalRelationshipName , Type type )
200
209
{
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
+ }
204
222
}
205
223
206
224
@@ -247,31 +265,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
247
265
/// <inheritdoc />
248
266
public virtual async Task < TEntity > UpdateAsync ( TEntity updatedEntity )
249
267
{
250
- var oldEntity = await GetAsync ( updatedEntity . Id ) ;
251
- if ( oldEntity == null )
268
+ var databaseEntity = await GetAsync ( updatedEntity . Id ) ;
269
+ if ( databaseEntity == null )
252
270
return null ;
253
271
254
272
foreach ( var attr in _jsonApiContext . AttributesToUpdate . Keys )
255
- attr . SetValue ( oldEntity , attr . GetValue ( updatedEntity ) ) ;
273
+ attr . SetValue ( databaseEntity , attr . GetValue ( updatedEntity ) ) ;
256
274
257
275
foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
258
276
{
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
261
285
LoadInverseRelationships ( trackedRelationshipValue , relationshipAttr ) ;
262
- AssignRelationshipValue ( oldEntity , trackedRelationshipValue , relationshipAttr ) ;
286
+ /// assigns the updated relationship to the database entity
287
+ AssignRelationshipValue ( databaseEntity , trackedRelationshipValue , relationshipAttr ) ;
263
288
}
264
289
265
290
await _context . SaveChangesAsync ( ) ;
266
- return oldEntity ;
291
+ return databaseEntity ;
267
292
}
268
293
269
294
270
295
/// <summary>
271
296
/// Responsible for getting the relationship value for a given relationship
272
297
/// attribute of a given entity. It ensures that the relationship value
273
298
/// 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.
275
301
/// </summary>
276
302
private object GetTrackedRelationshipValue ( RelationshipAttribute relationshipAttr , TEntity entity , out bool wasAlreadyAttached )
277
303
{
@@ -436,9 +462,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
436
462
437
463
/// <summary>
438
464
/// 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
440
466
/// it will not perform a complete-replace which is required for
441
467
/// 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]`.
442
475
/// </summary>
443
476
protected void LoadCurrentRelationships ( TEntity oldEntity , RelationshipAttribute relationshipAttribute )
444
477
{
@@ -454,21 +487,18 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
454
487
}
455
488
456
489
/// <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"/>
461
491
/// </summary>
462
- private void AssignRelationshipValue ( TEntity oldEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
492
+ private void AssignRelationshipValue ( TEntity targetEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
463
493
{
464
494
if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
465
495
{
466
496
// todo: this logic should be put in the HasManyThrough attribute
467
- AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
497
+ AssignHasManyThrough ( targetEntity , throughAttribute , ( IList ) relationshipValue ) ;
468
498
}
469
499
else
470
500
{
471
- relationshipAttribute . SetValue ( oldEntity , relationshipValue ) ;
501
+ relationshipAttribute . SetValue ( targetEntity , relationshipValue ) ;
472
502
}
473
503
}
474
504
0 commit comments