@@ -238,13 +238,15 @@ private void AttachHasMany(TEntity entity, HasManyAttribute relationship, IList
238
238
var relatedList = ( IList ) entity . GetType ( ) . GetProperty ( relationship . EntityPropertyName ) ? . GetValue ( entity ) ;
239
239
foreach ( var related in relatedList )
240
240
{
241
- _context . Entry ( related ) . State = EntityState . Unchanged ;
241
+ if ( _context . EntityIsTracked ( related as IIdentifiable ) == false )
242
+ _context . Entry ( related ) . State = EntityState . Unchanged ;
242
243
}
243
244
}
244
245
else
245
246
{
246
247
foreach ( var pointer in pointers )
247
248
{
249
+ if ( _context . EntityIsTracked ( pointer as IIdentifiable ) == false )
248
250
_context . Entry ( pointer ) . State = EntityState . Unchanged ;
249
251
}
250
252
}
@@ -261,7 +263,8 @@ private void AttachHasManyThrough(TEntity entity, HasManyThroughAttribute hasMan
261
263
262
264
foreach ( var pointer in pointers )
263
265
{
264
- _context . Entry ( pointer ) . State = EntityState . Unchanged ;
266
+ if ( _context . EntityIsTracked ( pointer as IIdentifiable ) == false )
267
+ _context . Entry ( pointer ) . State = EntityState . Unchanged ;
265
268
var throughInstance = Activator . CreateInstance ( hasManyThrough . ThroughType ) ;
266
269
267
270
hasManyThrough . LeftProperty . SetValue ( throughInstance , entity ) ;
@@ -311,21 +314,61 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
311
314
312
315
if ( _jsonApiContext . RelationshipsToUpdate . Any ( ) )
313
316
{
317
+ /// For one-to-many and many-to-many, the PATCH must perform a
318
+ /// complete replace. When assigning new relationship values,
319
+ /// it will only be like this if the to-be-replaced entities are loaded
320
+ foreach ( var relationship in _jsonApiContext . RelationshipsToUpdate )
321
+ {
322
+ if ( relationship . Key is HasManyThroughAttribute throughAttribute )
323
+ {
324
+ await _context . Entry ( oldEntity ) . Collection ( throughAttribute . InternalThroughName ) . LoadAsync ( ) ;
325
+ }
326
+ }
327
+
328
+ /// @HACK @TODO: It is inconsistent that for many-to-many, the new relationship value
329
+ /// is assigned in AttachRelationships() helper fn below, but not for
330
+ /// one-to-many and one-to-one (we need to do that manually as done below).
331
+ /// Simultaneously, for a proper working "complete replacement", in the case of many-to-many
332
+ /// we need to LoadAsync() BEFORE calling AttachRelationships(), but for one-to-many we
333
+ /// need to do it AFTER AttachRelationships or we we'll get entity tracking errors
334
+ /// This really needs a refactor.
314
335
AttachRelationships ( oldEntity ) ;
336
+
315
337
foreach ( var relationship in _jsonApiContext . RelationshipsToUpdate )
316
338
{
317
- /// If we are updating to-many relations from PATCH, we need to include the relation first,
318
- /// else it will not peform a complete replacement, as required by the specs.
319
- /// Also, we currently do not support the same for many-to-many
320
- if ( relationship . Key is HasManyAttribute && ! ( relationship . Key is HasManyThroughAttribute ) )
339
+ if ( ( relationship . Key . TypeId as Type ) . IsAssignableFrom ( typeof ( HasOneAttribute ) ) )
340
+ {
341
+ relationship . Key . SetValue ( oldEntity , relationship . Value ) ;
342
+ }
343
+ if ( ( relationship . Key . TypeId as Type ) . IsAssignableFrom ( typeof ( HasManyAttribute ) ) )
344
+ {
321
345
await _context . Entry ( oldEntity ) . Collection ( relationship . Key . InternalRelationshipName ) . LoadAsync ( ) ;
322
- relationship . Key . SetValue ( oldEntity , relationship . Value ) ; // article.tags = nieuwe lijst
346
+ var value = PreventReattachment ( ( IEnumerable < object > ) relationship . Value ) ;
347
+ relationship . Key . SetValue ( oldEntity , value ) ;
348
+ }
323
349
}
324
350
}
325
351
await _context . SaveChangesAsync ( ) ;
326
352
return oldEntity ;
327
353
}
328
354
355
+ /// <summary>
356
+ /// We need to make sure we're not re-attaching entities when assigning
357
+ /// new relationship values. Entities may have been loaded in the change
358
+ /// tracker anywhere in the application beyond the control of
359
+ /// JsonApiDotNetCore.
360
+ /// </summary>
361
+ /// <returns>The interpolated related entity collection</returns>
362
+ /// <param name="relatedEntities">Related entities.</param>
363
+ object PreventReattachment ( IEnumerable < object > relatedEntities )
364
+ {
365
+ var relatedType = TypeHelper . GetTypeOfList ( relatedEntities . GetType ( ) ) ;
366
+ var replaced = relatedEntities . Cast < IIdentifiable > ( ) . Select ( entity => _context . GetTrackedEntity ( entity ) ?? entity ) ;
367
+ return TypeHelper . ConvertCollection ( replaced , relatedType ) ;
368
+
369
+ }
370
+
371
+
329
372
/// <inheritdoc />
330
373
public async Task UpdateRelationshipsAsync ( object parent , RelationshipAttribute relationship , IEnumerable < string > relationshipIds )
331
374
{
0 commit comments