@@ -144,23 +144,29 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
144
144
/// <inheritdoc />
145
145
public virtual async Task < TEntity > CreateAsync ( TEntity entity )
146
146
{
147
- AttachRelationships ( entity ) ;
148
- AssignRelationshipValues ( entity ) ;
147
+ foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
148
+ {
149
+ var trackedRelationshipValue = GetTrackedRelationshipValue ( relationshipAttr , entity , out bool wasAlreadyTracked ) ;
150
+ // LoadInverseRelationships(trackedRelationshipValue, relationshipAttribute)
151
+ if ( wasAlreadyTracked )
152
+ {
153
+ /// We only need to reassign the relationship value to the to-be-added
154
+ /// entity when we're using a different instance (because this different one
155
+ /// was already tracked) than the one assigned to the to-be-created entity.
156
+ AssignRelationshipValue ( entity , trackedRelationshipValue , relationshipAttr ) ;
157
+ } else if ( relationshipAttr is HasManyThroughAttribute throughAttr )
158
+ {
159
+ /// even if we don't have to reassign anything because of already tracked
160
+ /// entities, we still need to assign the "through" entities in the case of many-to-many.
161
+ AssignHasManyThrough ( entity , throughAttr , ( IList ) trackedRelationshipValue ) ;
162
+ }
163
+ }
149
164
_dbSet . Add ( entity ) ;
150
-
151
165
await _context . SaveChangesAsync ( ) ;
152
166
153
167
return entity ;
154
168
}
155
169
156
- /// <summary>
157
-
158
- /// </summary>
159
- protected virtual void AttachRelationships ( TEntity entity = null )
160
- {
161
- AttachHasManyAndHasManyThroughPointers ( entity ) ;
162
- AttachHasOnePointers ( entity ) ;
163
- }
164
170
165
171
/// <inheritdoc />
166
172
public void DetachRelationshipPointers ( TEntity entity )
@@ -207,7 +213,7 @@ public void DetachRelationshipPointers(TEntity entity)
207
213
}
208
214
209
215
/// <inheritdoc />
210
- public virtual async Task < TEntity > UpdateAsync ( TId id , TEntity entity )
216
+ public virtual async Task < TEntity > UpdateAsync ( TId id , TEntity updatedEntity )
211
217
{
212
218
/// WHY is parameter "entity" even passed along to this method??
213
219
/// It does nothing!
@@ -217,25 +223,73 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
217
223
if ( oldEntity == null )
218
224
return null ;
219
225
220
- foreach ( var attr in _jsonApiContext . AttributesToUpdate )
221
- attr . Key . SetValue ( oldEntity , attr . Value ) ;
226
+ foreach ( var attr in _jsonApiContext . AttributesToUpdate . Keys )
227
+ attr . SetValue ( oldEntity , attr . GetValue ( updatedEntity ) ) ;
222
228
223
- if ( _jsonApiContext . RelationshipsToUpdate . Any ( ) )
229
+ foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
224
230
{
225
- /// First attach all targeted relationships to the dbcontext.
226
- /// This takes into account that some of these entities are
227
- /// already attached in the dbcontext
228
- AttachRelationships ( oldEntity ) ;
229
- /// load the current state of the relationship to support complete-replacement
230
- LoadCurrentRelationships ( oldEntity ) ;
231
- /// assign the actual relationship values.
232
- AssignRelationshipValues ( oldEntity ) ;
231
+ LoadCurrentRelationships ( oldEntity , relationshipAttr ) ;
232
+ var trackedRelationshipValue = GetTrackedRelationshipValue ( relationshipAttr , updatedEntity , out bool wasAlreadyTracked ) ;
233
+ // LoadInverseRelationships(trackedRelationshipValue, relationshipAttribute)
234
+ AssignRelationshipValue ( oldEntity , trackedRelationshipValue , relationshipAttr ) ;
233
235
}
234
236
await _context . SaveChangesAsync ( ) ;
235
237
return oldEntity ;
236
238
}
237
- /// <inheritdoc />
238
239
240
+
241
+ /// <summary>
242
+ /// Responsible for getting the relationship value for a given relationship
243
+ /// attribute of a given entity. It ensures that the relationship value
244
+ /// that it returns is attached to the database without reattaching duplicates instances
245
+ /// to the change tracker.
246
+ /// </summary>
247
+ private object GetTrackedRelationshipValue ( RelationshipAttribute relationshipAttr , TEntity entity , out bool wasAlreadyAttached )
248
+ {
249
+ wasAlreadyAttached = false ;
250
+ if ( relationshipAttr is HasOneAttribute hasOneAttribute )
251
+ {
252
+ /// This adds support for resource-entity separation in the case of one-to-one.
253
+ var relationshipValue = GetEntityResourceSeparationValue ( entity , hasOneAttribute ) ?? ( IIdentifiable ) hasOneAttribute . GetValue ( entity ) ;
254
+ if ( relationshipValue == null )
255
+ return null ;
256
+ return GetTrackedHasOneRelationshipValue ( relationshipValue , hasOneAttribute , ref wasAlreadyAttached ) ;
257
+ }
258
+ else
259
+ {
260
+ IEnumerable < IIdentifiable > relationshipValueList = ( IEnumerable < IIdentifiable > ) relationshipAttr . GetValue ( entity ) ;
261
+ /// This adds support for resource-entity separation in the case of one-to-many.
262
+ /// todo: currently there is no support for many to many relations.
263
+ if ( relationshipAttr is HasManyAttribute hasMany )
264
+ relationshipValueList = GetEntityResourceSeparationValue ( entity , hasMany ) ?? relationshipValueList ;
265
+ if ( relationshipValueList == null ) return null ;
266
+ return GetTrackedManyRelationshipValue ( relationshipValueList , relationshipAttr , ref wasAlreadyAttached ) ;
267
+ }
268
+ }
269
+
270
+ private IList GetTrackedManyRelationshipValue ( IEnumerable < IIdentifiable > relationshipValueList , RelationshipAttribute relationshipAttr , ref bool wasAlreadyAttached )
271
+ {
272
+ if ( relationshipValueList == null ) return null ;
273
+ bool _wasAlreadyAttached = false ;
274
+ var trackedPointerCollection = relationshipValueList . Select ( pointer =>
275
+ {
276
+ var tracked = AttachOrGetTracked ( pointer ) ;
277
+ if ( tracked != null ) _wasAlreadyAttached = true ;
278
+ return Convert . ChangeType ( tracked ?? pointer , relationshipAttr . Type ) ;
279
+ } ) . ToList ( ) . Cast ( relationshipAttr . Type ) ;
280
+ if ( _wasAlreadyAttached ) wasAlreadyAttached = true ;
281
+ return ( IList ) trackedPointerCollection ;
282
+ }
283
+
284
+ private IIdentifiable GetTrackedHasOneRelationshipValue ( IIdentifiable relationshipValue , HasOneAttribute hasOneAttribute , ref bool wasAlreadyAttached )
285
+ {
286
+
287
+ var tracked = AttachOrGetTracked ( relationshipValue ) ;
288
+ if ( tracked != null ) wasAlreadyAttached = true ;
289
+ return tracked ?? relationshipValue ;
290
+ }
291
+
292
+ /// <inheritdoc />
239
293
public async Task UpdateRelationshipsAsync ( object parent , RelationshipAttribute relationship , IEnumerable < string > relationshipIds )
240
294
{
241
295
// TODO: it would be better to let this be determined within the relationship attribute...
@@ -343,61 +397,23 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
343
397
: entities . ToList ( ) ;
344
398
}
345
399
400
+
346
401
/// <summary>
347
- /// This is used to allow creation of HasMany relationships when the
348
- /// dependent side of the relationship already exists.
402
+ /// Before assigning new relationship values (UpdateAsync), we need to
403
+ /// attach the current relationship state to the dbcontext, else
404
+ /// it will not perform a complete-replace which is required for
405
+ /// one-to-many and many-to-many.
349
406
/// </summary>
350
- private void AttachHasManyAndHasManyThroughPointers ( TEntity entity )
407
+ protected void LoadCurrentRelationships ( TEntity oldEntity , RelationshipAttribute relationshipAttribute )
351
408
{
352
- var relationships = _jsonApiContext . HasManyRelationshipPointers . Get ( ) ;
353
-
354
- foreach ( var attribute in relationships . Keys . ToArray ( ) )
409
+ if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
355
410
{
356
- IEnumerable < IIdentifiable > pointers ;
357
- if ( attribute is HasManyThroughAttribute hasManyThrough )
358
- {
359
- pointers = relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
360
- }
361
- else
362
- {
363
- pointers = GetRelationshipPointers ( entity , ( HasManyAttribute ) attribute ) ?? relationships [ attribute ] . Cast < IIdentifiable > ( ) ;
364
- }
411
+ _context . Entry ( oldEntity ) . Collection ( throughAttribute . InternalThroughName ) . Load ( ) ;
365
412
366
- if ( pointers == null ) continue ;
367
- bool alreadyTracked = false ;
368
- Type entityType = null ;
369
- var newPointerCollection = pointers . Select ( pointer =>
370
- {
371
- entityType = pointer . GetType ( ) ;
372
- var tracked = AttachOrGetTracked ( pointer ) ;
373
- if ( tracked != null ) alreadyTracked = true ;
374
- return Convert . ChangeType ( tracked ?? pointer , entityType ) ;
375
- } ) . ToList ( ) . Cast ( entityType ) ;
376
-
377
- if ( alreadyTracked || pointers != relationships [ attribute ] ) relationships [ attribute ] = ( IList ) newPointerCollection ;
378
413
}
379
- }
380
-
381
- /// <summary>
382
- /// Before assigning new relationship values (updateasync), we need to
383
- /// attach load the current relationship state into the dbcontext, else
384
- /// there won't be a complete-replace for one-to-many and many-to-many.
385
- /// </summary>
386
- /// <param name="oldEntity">Old entity.</param>
387
- protected void LoadCurrentRelationships ( TEntity oldEntity )
388
- {
389
- foreach ( var relationshipEntry in _jsonApiContext . RelationshipsToUpdate )
414
+ else if ( relationshipAttribute is HasManyAttribute hasManyAttribute )
390
415
{
391
- var relationshipValue = relationshipEntry . Value ;
392
- if ( relationshipEntry . Key is HasManyThroughAttribute throughAttribute )
393
- {
394
- _context . Entry ( oldEntity ) . Collection ( throughAttribute . InternalThroughName ) . Load ( ) ;
395
-
396
- }
397
- else if ( relationshipEntry . Key is HasManyAttribute hasManyAttribute )
398
- {
399
- _context . Entry ( oldEntity ) . Collection ( hasManyAttribute . InternalRelationshipName ) . Load ( ) ;
400
- }
416
+ _context . Entry ( oldEntity ) . Collection ( hasManyAttribute . InternalRelationshipName ) . Load ( ) ;
401
417
}
402
418
}
403
419
@@ -407,25 +423,16 @@ protected void LoadCurrentRelationships(TEntity oldEntity)
407
423
/// retrieve from the context WHICH relationships to update, but the actual
408
424
/// values should not come from the context.
409
425
/// </summary>
410
- protected void AssignRelationshipValues ( TEntity oldEntity )
426
+ protected void AssignRelationshipValue ( TEntity oldEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
411
427
{
412
- foreach ( var relationshipEntry in _jsonApiContext . RelationshipsToUpdate )
428
+ if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
413
429
{
414
- var relationshipValue = relationshipEntry . Value ;
415
- if ( relationshipEntry . Key is HasManyThroughAttribute throughAttribute )
416
- {
417
- AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
418
- }
419
- else if ( relationshipEntry . Key is HasManyAttribute hasManyAttribute )
420
- {
421
- // todo: need to load inverse relationship here, see issue #502
422
- hasManyAttribute . SetValue ( oldEntity , relationshipValue ) ;
423
- }
424
- else if ( relationshipEntry . Key is HasOneAttribute hasOneAttribute )
425
- {
426
- // todo: need to load inverse relationship here, see issue #502
427
- hasOneAttribute . SetValue ( oldEntity , relationshipValue ) ;
428
- }
430
+ // todo: this logic should be put in the HasManyThrough attribute
431
+ AssignHasManyThrough ( oldEntity , throughAttribute , ( IList ) relationshipValue ) ;
432
+ }
433
+ else
434
+ {
435
+ relationshipAttribute . SetValue ( oldEntity , relationshipValue ) ;
429
436
}
430
437
}
431
438
@@ -451,33 +458,23 @@ private void AssignHasManyThrough(TEntity entity, HasManyThroughAttribute hasMan
451
458
}
452
459
453
460
/// <summary>
454
- /// This is used to allow creation of HasOne relationships when the
461
+ /// A helper method that gets the relationship value in the case of
462
+ /// entity resource separation.
455
463
/// </summary>
456
- private void AttachHasOnePointers ( TEntity entity )
457
- {
458
- var relationships = _jsonApiContext
459
- . HasOneRelationshipPointers
460
- . Get ( ) ;
461
-
462
- foreach ( var attribute in relationships . Keys . ToArray ( ) )
463
- {
464
- var pointer = GetRelationshipPointer ( entity , attribute ) ?? relationships [ attribute ] ;
465
- if ( pointer == null ) return ;
466
- var tracked = AttachOrGetTracked ( pointer ) ;
467
- if ( tracked != null || pointer != relationships [ attribute ] ) relationships [ attribute ] = tracked ?? pointer ;
468
- }
469
- }
470
-
471
- IIdentifiable GetRelationshipPointer ( TEntity principalEntity , HasOneAttribute attribute )
464
+ IIdentifiable GetEntityResourceSeparationValue ( TEntity entity , HasOneAttribute attribute )
472
465
{
473
466
if ( attribute . EntityPropertyName == null )
474
467
{
475
468
return null ;
476
469
}
477
- return ( IIdentifiable ) principalEntity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( principalEntity ) ;
470
+ return ( IIdentifiable ) entity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( entity ) ;
478
471
}
479
472
480
- IEnumerable < IIdentifiable > GetRelationshipPointers ( TEntity entity , HasManyAttribute attribute )
473
+ /// <summary>
474
+ /// A helper method that gets the relationship value in the case of
475
+ /// entity resource separation.
476
+ /// </summary>
477
+ IEnumerable < IIdentifiable > GetEntityResourceSeparationValue ( TEntity entity , HasManyAttribute attribute )
481
478
{
482
479
if ( attribute . EntityPropertyName == null )
483
480
{
@@ -486,23 +483,31 @@ IEnumerable<IIdentifiable> GetRelationshipPointers(TEntity entity, HasManyAttrib
486
483
return ( ( IEnumerable ) ( entity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( entity ) ) ) . Cast < IIdentifiable > ( ) ;
487
484
}
488
485
489
- // useful article: https://stackoverflow.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified
490
- IIdentifiable AttachOrGetTracked ( IIdentifiable pointer )
486
+ /// <summary>
487
+ /// Given a iidentifiable relationshipvalue, verify if an entity of the underlying
488
+ /// type with the same ID is already attached to the dbContext, and if so, return it.
489
+ /// If not, attach the relationship value to the dbContext.
490
+ ///
491
+ /// useful article: https://stackoverflow.com/questions/30987806/dbset-attachentity-vs-dbcontext-entryentity-state-entitystate-modified
492
+ /// </summary>
493
+ IIdentifiable AttachOrGetTracked ( IIdentifiable relationshipValue )
491
494
{
492
- var trackedEntity = _context . GetTrackedEntity ( pointer ) ;
495
+ var trackedEntity = _context . GetTrackedEntity ( relationshipValue ) ;
493
496
494
497
if ( trackedEntity != null )
495
498
{
496
499
/// there already was an instance of this type and ID tracked
497
- /// by EF Core. Reattaching will produce a conflict, and from now on we
498
- /// will use the already attached one instead. ( This entry might
499
- /// contain updated fields as a result of business logic)
500
+ /// by EF Core. Reattaching will produce a conflict, so from now on we
501
+ /// will use the already attached instance instead. This entry might
502
+ /// contain updated fields as a result of business logic elsewhere in the application
500
503
return trackedEntity ;
501
504
}
502
505
503
506
/// the relationship pointer is new to EF Core, but we are sure
504
- /// it exists in the database (json:api spec), so we attach it.
505
- _context . Entry ( pointer ) . State = EntityState . Unchanged ;
507
+ /// it exists in the database, so we attach it. In this case, as per
508
+ /// the json:api spec, we can also safely assume that no fields of
509
+ /// this entity were updated.
510
+ _context . Entry ( relationshipValue ) . State = EntityState . Unchanged ;
506
511
return null ;
507
512
}
508
513
}
0 commit comments