@@ -158,7 +158,7 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
158
158
159
159
protected virtual void AttachRelationships ( TEntity entity = null )
160
160
{
161
- AttachHasManyPointers ( entity ) ;
161
+ AttachHasManyAndHasManyThroughPointers ( entity ) ;
162
162
AttachHasOnePointers ( entity ) ;
163
163
}
164
164
@@ -206,159 +206,15 @@ public void DetachRelationshipPointers(TEntity entity)
206
206
}
207
207
}
208
208
209
- /// <summary>
210
- /// This is used to allow creation of HasMany relationships when the
211
- /// dependent side of the relationship already exists.
212
- /// </summary>
213
- private void AttachHasManyAndHasManyThroughPointers ( TEntity entity )
214
- {
215
- var relationships = _jsonApiContext . HasManyRelationshipPointers . Get ( ) ;
216
-
217
- foreach ( var attribute in relationships . Keys )
218
- {
219
- IEnumerable < IIdentifiable > pointers ;
220
- if ( attribute is HasManyThroughAttribute hasManyThrough )
221
- {
222
- pointers = relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
223
- }
224
- else
225
- {
226
- pointers = GetRelationshipPointers ( entity , ( HasManyAttribute ) attribute ) ?? relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
227
- }
228
-
229
- if ( pointers == null ) continue ;
230
- bool alreadyTracked = false ;
231
- var newPointerCollection = pointers . Select ( pointer =>
232
- {
233
- var tracked = AttachOrGetTracked ( pointer ) ;
234
- if ( tracked != null ) alreadyTracked = true ;
235
- return tracked ?? pointer ;
236
- } ) ;
237
-
238
- if ( alreadyTracked ) relationships [ attribute ] = ( IList ) newPointerCollection ;
239
- }
240
- }
241
-
242
- private void AttachHasMany ( TEntity entity , RelationshipAttribute attribute , Dictionary < RelationshipAttribute , IList > relationships )
243
- {
244
- IEnumerable < IIdentifiable > pointers ;
245
- if ( attribute is HasManyThroughAttribute hasManyThrough )
246
- {
247
- pointers = relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
248
- } else
249
- {
250
- pointers = GetRelationshipPointers ( entity , ( HasManyAttribute ) attribute ) ?? relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
251
- }
252
-
253
- if ( pointers == null ) return ;
254
- bool alreadyTracked = false ;
255
- var newPointerCollection = pointers . Select ( pointer =>
256
- {
257
- var tracked = AttachOrGetTracked ( pointer ) ;
258
- if ( tracked != null ) alreadyTracked = true ;
259
- return tracked ?? pointer ;
260
- } ) ;
261
-
262
- if ( alreadyTracked ) relationships [ attribute ] = ( IList ) newPointerCollection ;
263
- }
264
-
265
- private void AttachHasManyThrough ( TEntity entity , HasManyThroughAttribute hasManyThrough , Dictionary < RelationshipAttribute , IList > relationships )
266
- {
267
- var pointers = relationships [ hasManyThrough ] . Cast < IIdentifiable > ( ) ;
268
- bool alreadyTracked = false ;
269
- var newPointerCollection = pointers . Select ( pointer =>
270
- {
271
- var tracked = AttachOrGetTracked ( pointer ) ;
272
- if ( tracked != null ) alreadyTracked = true ;
273
- return tracked ?? pointer ;
274
- } ) ;
275
- if ( alreadyTracked ) relationships [ hasManyAttribute ] = ( IList ) newPointerCollection ;
276
-
277
- foreach ( var pointer in pointers )
278
- {
279
- if ( _context . EntityIsTracked ( pointer as IIdentifiable ) == false )
280
- _context . Entry ( pointer ) . State = EntityState . Unchanged ;
281
-
282
- var throughRelationshipCollection = Activator . CreateInstance ( hasManyThrough . ThroughProperty . PropertyType ) as IList ;
283
- hasManyThrough . ThroughProperty . SetValue ( entity , throughRelationshipCollection ) ;
284
-
285
- foreach ( var pointer in pointers )
286
- {
287
- if ( _context . EntityIsTracked ( pointer as IIdentifiable ) == false )
288
- _context . Entry ( pointer ) . State = EntityState . Unchanged ;
289
- var throughInstance = Activator . CreateInstance ( hasManyThrough . ThroughType ) ;
290
209
291
- hasManyThrough . LeftProperty . SetValue ( throughInstance , entity ) ;
292
- hasManyThrough . RightProperty . SetValue ( throughInstance , pointer ) ;
293
-
294
- throughRelationshipCollection . Add ( throughInstance ) ;
295
- }
296
- }
297
- }
298
-
299
- /// <summary>
300
- /// This is used to allow creation of HasOne relationships when the
301
- /// </summary>
302
- private void AttachHasOnePointers ( TEntity entity )
303
- {
304
- var relationships = _jsonApiContext
305
- . HasOneRelationshipPointers
306
- . Get ( ) ;
307
-
308
- foreach ( var relationship in relationships )
309
- {
310
- var pointer = GetRelationshipPointer ( entity , relationship ) ;
311
- if ( pointer == null ) return ;
312
- var tracked = AttachOrGetTracked ( pointer ) ;
313
- if ( tracked != null ) relationships [ relationship . Key ] = tracked ;
314
- }
315
- }
316
-
317
- IIdentifiable GetRelationshipPointer ( TEntity principalEntity , KeyValuePair < HasOneAttribute , IIdentifiable > relationship )
318
- {
319
- HasOneAttribute hasOne = relationship . Key ;
320
- if ( hasOne . EntityPropertyName != null )
321
- {
322
- var relatedEntity = principalEntity . GetType ( ) . GetProperty ( hasOne . EntityPropertyName ) ? . GetValue ( principalEntity ) ;
323
- return ( IIdentifiable ) relatedEntity ;
324
- }
325
- return relationship . Value ;
326
- }
327
-
328
- IEnumerable < IIdentifiable > GetRelationshipPointers ( TEntity entity , HasManyAttribute attribute )
329
- {
330
- if ( attribute . EntityPropertyName == null )
331
- {
332
- return null ;
333
- }
334
- return ( ( IEnumerable ) ( entity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( entity ) ) ) . Cast < IIdentifiable > ( ) ;
335
- }
336
-
337
- // useful article: https://stackoverflow.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified
338
- IIdentifiable AttachOrGetTracked ( IIdentifiable pointer )
339
- {
340
- var trackedEntity = _context . GetTrackedEntity ( pointer ) ;
341
-
342
-
343
- if ( trackedEntity != null )
344
- {
345
- /// there already was an instance of this type and ID tracked
346
- /// by EF Core. Reattaching will produce a conflict, and from now on we
347
- /// will use the already attached one instead. (This entry might
348
- /// contain updated fields as a result of business logic)
349
- return trackedEntity ;
350
- }
351
-
352
- /// the relationship pointer is new to EF Core, but we are sure
353
- /// it exists in the database (json:api spec), so we attach it.
354
- _context . Entry ( pointer ) . State = EntityState . Unchanged ;
355
- return null ;
356
- }
357
210
358
211
359
212
/// <inheritdoc />
360
213
public virtual async Task < TEntity > UpdateAsync ( TId id , TEntity entity )
361
214
{
215
+ /// WHY is parameter "entity" even passed along to this method??
216
+ /// It does nothing!
217
+
362
218
var oldEntity = await GetAsync ( id ) ;
363
219
364
220
if ( oldEntity == null )
@@ -369,58 +225,44 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
369
225
370
226
if ( _jsonApiContext . RelationshipsToUpdate . Any ( ) )
371
227
{
372
- /// For one-to-many and many-to-many, the PATCH must perform a
373
- /// complete replace. When assigning new relationship values,
374
- /// it will only be like this if the to-be-replaced entities are loaded
375
- foreach ( var relationship in _jsonApiContext . RelationshipsToUpdate )
228
+ AttachRelationships ( oldEntity ) ;
229
+ foreach ( var relationshipEntry in _jsonApiContext . RelationshipsToUpdate )
376
230
{
377
- if ( relationship . Key is HasManyThroughAttribute throughAttribute )
231
+ var relationshipValue = relationshipEntry . Value ;
232
+ if ( relationshipEntry . Key is HasManyThroughAttribute throughAttribute )
378
233
{
234
+ // load relations to enforce complete replace
379
235
await _context . Entry ( oldEntity ) . Collection ( throughAttribute . InternalThroughName ) . LoadAsync ( ) ;
380
- }
381
- }
382
-
383
- /// @HACK @TODO: It is inconsistent that for many-to-many, the new relationship value
384
- /// is assigned in AttachRelationships() helper fn below, but not for
385
- /// one-to-many and one-to-one (we need to do that manually as done below).
386
- /// Simultaneously, for a proper working "complete replacement", in the case of many-to-many
387
- /// we need to LoadAsync() BEFORE calling AttachRelationships(), but for one-to-many we
388
- /// need to do it AFTER AttachRelationships or we we'll get entity tracking errors
389
- /// This really needs a refactor.
390
- AttachRelationships ( oldEntity ) ;
391
-
392
- foreach ( var relationship in _jsonApiContext . RelationshipsToUpdate )
393
- {
394
- if ( ( relationship . Key . TypeId as Type ) . IsAssignableFrom ( typeof ( HasOneAttribute ) ) )
236
+ AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
237
+ } else if ( relationshipEntry . Key is HasManyAttribute hasManyAttribute )
395
238
{
396
- relationship . Key . SetValue ( oldEntity , relationship . Value ) ;
239
+ // load relations to enforce complete replace
240
+ await _context . Entry ( oldEntity ) . Collection ( hasManyAttribute . InternalRelationshipName ) . LoadAsync ( ) ;
241
+ hasManyAttribute . SetValue ( oldEntity , relationshipValue ) ;
397
242
}
398
- if ( ( relationship . Key . TypeId as Type ) . IsAssignableFrom ( typeof ( HasManyAttribute ) ) )
243
+ else if ( relationshipEntry . Key is HasOneAttribute hasOneAttribute )
399
244
{
400
- await _context . Entry ( oldEntity ) . Collection ( relationship . Key . InternalRelationshipName ) . LoadAsync ( ) ;
401
- var value = PreventReattachment ( ( IEnumerable < object > ) relationship . Value ) ;
402
- relationship . Key . SetValue ( oldEntity , value ) ;
245
+ hasOneAttribute . SetValue ( oldEntity , relationshipValue ) ;
403
246
}
404
247
}
405
248
}
406
249
await _context . SaveChangesAsync ( ) ;
407
250
return oldEntity ;
408
251
}
409
252
410
- /// <summary>
411
- /// We need to make sure we're not re-attaching entities when assigning
412
- /// new relationship values. Entities may have been loaded in the change
413
- /// tracker anywhere in the application beyond the control of
414
- /// JsonApiDotNetCore.
415
- /// </summary>
416
- /// <returns>The interpolated related entity collection</returns>
417
- /// <param name="relatedEntities">Related entities.</param>
418
- object PreventReattachment ( IEnumerable < object > relatedEntities )
253
+ private void AssignHasManyThrough ( TEntity entity , HasManyThroughAttribute hasManyThrough , IList relationshipValue )
419
254
{
420
- var relatedType = TypeHelper . GetTypeOfList ( relatedEntities . GetType ( ) ) ;
421
- var replaced = relatedEntities . Cast < IIdentifiable > ( ) . Select ( entity => _context . GetTrackedEntity ( entity ) ?? entity ) ;
422
- return TypeHelper . ConvertCollection ( replaced , relatedType ) ;
255
+ var pointers = relationshipValue . Cast < IIdentifiable > ( ) ;
256
+ var throughRelationshipCollection = Activator . CreateInstance ( hasManyThrough . ThroughProperty . PropertyType ) as IList ;
257
+ hasManyThrough . ThroughProperty . SetValue ( entity , throughRelationshipCollection ) ;
423
258
259
+ foreach ( var pointer in pointers )
260
+ {
261
+ var throughInstance = Activator . CreateInstance ( hasManyThrough . ThroughType ) ;
262
+ hasManyThrough . LeftProperty . SetValue ( throughInstance , entity ) ;
263
+ hasManyThrough . RightProperty . SetValue ( throughInstance , pointer ) ;
264
+ throughRelationshipCollection . Add ( throughInstance ) ;
265
+ }
424
266
}
425
267
426
268
@@ -535,5 +377,96 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
535
377
? await entities . ToListAsync ( )
536
378
: entities . ToList ( ) ;
537
379
}
380
+
381
+ /// <summary>
382
+ /// This is used to allow creation of HasMany relationships when the
383
+ /// dependent side of the relationship already exists.
384
+ /// </summary>
385
+ private void AttachHasManyAndHasManyThroughPointers ( TEntity entity )
386
+ {
387
+ var relationships = _jsonApiContext . HasManyRelationshipPointers . Get ( ) ;
388
+
389
+ foreach ( var attribute in relationships . Keys . ToArray ( ) )
390
+ {
391
+ IEnumerable < IIdentifiable > pointers ;
392
+ if ( attribute is HasManyThroughAttribute hasManyThrough )
393
+ {
394
+ pointers = relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
395
+ }
396
+ else
397
+ {
398
+ pointers = GetRelationshipPointers ( entity , ( HasManyAttribute ) attribute ) ?? relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
399
+ }
400
+
401
+ if ( pointers == null ) continue ;
402
+ bool alreadyTracked = false ;
403
+ var newPointerCollection = pointers . Select ( pointer =>
404
+ {
405
+ var tracked = AttachOrGetTracked ( pointer ) ;
406
+ if ( tracked != null ) alreadyTracked = true ;
407
+ return tracked ?? pointer ;
408
+ } ) ;
409
+
410
+ if ( alreadyTracked ) relationships [ attribute ] = ( IList ) newPointerCollection ;
411
+ }
412
+ }
413
+
414
+
415
+
416
+ /// <summary>
417
+ /// This is used to allow creation of HasOne relationships when the
418
+ /// </summary>
419
+ private void AttachHasOnePointers ( TEntity entity )
420
+ {
421
+ var relationships = _jsonApiContext
422
+ . HasOneRelationshipPointers
423
+ . Get ( ) ;
424
+
425
+ foreach ( var attribute in relationships . Keys . ToArray ( ) )
426
+ {
427
+ var pointer = GetRelationshipPointer ( entity , attribute ) ?? relationships [ attribute ] ;
428
+ if ( pointer == null ) return ;
429
+ var tracked = AttachOrGetTracked ( pointer ) ;
430
+ if ( tracked != null ) relationships [ attribute ] = tracked ;
431
+ }
432
+ }
433
+
434
+ IIdentifiable GetRelationshipPointer ( TEntity principalEntity , HasOneAttribute attribute )
435
+ {
436
+ if ( attribute . EntityPropertyName == null )
437
+ {
438
+ return null ;
439
+ }
440
+ return ( IIdentifiable ) principalEntity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( principalEntity ) ;
441
+ }
442
+
443
+ IEnumerable < IIdentifiable > GetRelationshipPointers ( TEntity entity , HasManyAttribute attribute )
444
+ {
445
+ if ( attribute . EntityPropertyName == null )
446
+ {
447
+ return null ;
448
+ }
449
+ return ( ( IEnumerable ) ( entity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( entity ) ) ) . Cast < IIdentifiable > ( ) ;
450
+ }
451
+
452
+ // useful article: https://stackoverflow.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified
453
+ IIdentifiable AttachOrGetTracked ( IIdentifiable pointer )
454
+ {
455
+ var trackedEntity = _context . GetTrackedEntity ( pointer ) ;
456
+
457
+ if ( trackedEntity != null )
458
+ {
459
+ /// there already was an instance of this type and ID tracked
460
+ /// by EF Core. Reattaching will produce a conflict, and from now on we
461
+ /// will use the already attached one instead. (This entry might
462
+ /// contain updated fields as a result of business logic)
463
+ return trackedEntity ;
464
+ }
465
+
466
+ /// the relationship pointer is new to EF Core, but we are sure
467
+ /// it exists in the database (json:api spec), so we attach it.
468
+ _context . Entry ( pointer ) . State = EntityState . Unchanged ;
469
+ return null ;
470
+ }
538
471
}
539
472
}
0 commit comments