@@ -144,23 +144,29 @@ public virtual async Task<TEntity> GetAndIncludeAsync(TId id, string relationshi
144144 /// <inheritdoc />
145145 public virtual async Task < TEntity > CreateAsync ( TEntity entity )
146146 {
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+ }
149164 _dbSet . Add ( entity ) ;
150-
151165 await _context . SaveChangesAsync ( ) ;
152166
153167 return entity ;
154168 }
155169
156- /// <summary>
157-
158- /// </summary>
159- protected virtual void AttachRelationships ( TEntity entity = null )
160- {
161- AttachHasManyAndHasManyThroughPointers ( entity ) ;
162- AttachHasOnePointers ( entity ) ;
163- }
164170
165171 /// <inheritdoc />
166172 public void DetachRelationshipPointers ( TEntity entity )
@@ -207,7 +213,7 @@ public void DetachRelationshipPointers(TEntity entity)
207213 }
208214
209215 /// <inheritdoc />
210- public virtual async Task < TEntity > UpdateAsync ( TId id , TEntity entity )
216+ public virtual async Task < TEntity > UpdateAsync ( TId id , TEntity updatedEntity )
211217 {
212218 /// WHY is parameter "entity" even passed along to this method??
213219 /// It does nothing!
@@ -217,25 +223,73 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
217223 if ( oldEntity == null )
218224 return null ;
219225
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 ) ) ;
222228
223- if ( _jsonApiContext . RelationshipsToUpdate . Any ( ) )
229+ foreach ( var relationshipAttr in _jsonApiContext . RelationshipsToUpdate ? . Keys )
224230 {
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 ) ;
233235 }
234236 await _context . SaveChangesAsync ( ) ;
235237 return oldEntity ;
236238 }
237- /// <inheritdoc />
238239
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 />
239293 public async Task UpdateRelationshipsAsync ( object parent , RelationshipAttribute relationship , IEnumerable < string > relationshipIds )
240294 {
241295 // 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
343397 : entities . ToList ( ) ;
344398 }
345399
400+
346401 /// <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.
349406 /// </summary>
350- private void AttachHasManyAndHasManyThroughPointers ( TEntity entity )
407+ protected void LoadCurrentRelationships ( TEntity oldEntity , RelationshipAttribute relationshipAttribute )
351408 {
352- var relationships = _jsonApiContext . HasManyRelationshipPointers . Get ( ) ;
353-
354- foreach ( var attribute in relationships . Keys . ToArray ( ) )
409+ if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
355410 {
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 ( ) ;
365412
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 ;
378413 }
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 )
390415 {
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 ( ) ;
401417 }
402418 }
403419
@@ -407,25 +423,16 @@ protected void LoadCurrentRelationships(TEntity oldEntity)
407423 /// retrieve from the context WHICH relationships to update, but the actual
408424 /// values should not come from the context.
409425 /// </summary>
410- protected void AssignRelationshipValues ( TEntity oldEntity )
426+ protected void AssignRelationshipValue ( TEntity oldEntity , object relationshipValue , RelationshipAttribute relationshipAttribute )
411427 {
412- foreach ( var relationshipEntry in _jsonApiContext . RelationshipsToUpdate )
428+ if ( relationshipAttribute is HasManyThroughAttribute throughAttribute )
413429 {
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 ) ;
429436 }
430437 }
431438
@@ -451,33 +458,23 @@ private void AssignHasManyThrough(TEntity entity, HasManyThroughAttribute hasMan
451458 }
452459
453460 /// <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.
455463 /// </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 )
472465 {
473466 if ( attribute . EntityPropertyName == null )
474467 {
475468 return null ;
476469 }
477- return ( IIdentifiable ) principalEntity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( principalEntity ) ;
470+ return ( IIdentifiable ) entity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( entity ) ;
478471 }
479472
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 )
481478 {
482479 if ( attribute . EntityPropertyName == null )
483480 {
@@ -486,23 +483,31 @@ IEnumerable<IIdentifiable> GetRelationshipPointers(TEntity entity, HasManyAttrib
486483 return ( ( IEnumerable ) ( entity . GetType ( ) . GetProperty ( attribute . EntityPropertyName ) ? . GetValue ( entity ) ) ) . Cast < IIdentifiable > ( ) ;
487484 }
488485
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 )
491494 {
492- var trackedEntity = _context . GetTrackedEntity ( pointer ) ;
495+ var trackedEntity = _context . GetTrackedEntity ( relationshipValue ) ;
493496
494497 if ( trackedEntity != null )
495498 {
496499 /// 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
500503 return trackedEntity ;
501504 }
502505
503506 /// 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 ;
506511 return null ;
507512 }
508513 }
0 commit comments