@@ -158,7 +158,7 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
158158
159159 protected virtual void AttachRelationships ( TEntity entity = null )
160160 {
161- AttachHasManyPointers ( entity ) ;
161+ AttachHasManyAndHasManyThroughPointers ( entity ) ;
162162 AttachHasOnePointers ( entity ) ;
163163 }
164164
@@ -206,159 +206,15 @@ public void DetachRelationshipPointers(TEntity entity)
206206 }
207207 }
208208
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 ) ;
290209
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- }
357210
358211
359212 /// <inheritdoc />
360213 public virtual async Task < TEntity > UpdateAsync ( TId id , TEntity entity )
361214 {
215+ /// WHY is parameter "entity" even passed along to this method??
216+ /// It does nothing!
217+
362218 var oldEntity = await GetAsync ( id ) ;
363219
364220 if ( oldEntity == null )
@@ -369,58 +225,44 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity entity)
369225
370226 if ( _jsonApiContext . RelationshipsToUpdate . Any ( ) )
371227 {
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 )
376230 {
377- if ( relationship . Key is HasManyThroughAttribute throughAttribute )
231+ var relationshipValue = relationshipEntry . Value ;
232+ if ( relationshipEntry . Key is HasManyThroughAttribute throughAttribute )
378233 {
234+ // load relations to enforce complete replace
379235 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 )
395238 {
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 ) ;
397242 }
398- if ( ( relationship . Key . TypeId as Type ) . IsAssignableFrom ( typeof ( HasManyAttribute ) ) )
243+ else if ( relationshipEntry . Key is HasOneAttribute hasOneAttribute )
399244 {
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 ) ;
403246 }
404247 }
405248 }
406249 await _context . SaveChangesAsync ( ) ;
407250 return oldEntity ;
408251 }
409252
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 )
419254 {
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 ) ;
423258
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+ }
424266 }
425267
426268
@@ -535,5 +377,96 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
535377 ? await entities . ToListAsync ( )
536378 : entities . ToList ( ) ;
537379 }
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+ }
538471 }
539472}
0 commit comments