Skip to content

Commit 1420d6d

Browse files
committed
refactor assign relationships and applied to updateasync method
1 parent e8d1096 commit 1420d6d

File tree

7 files changed

+137
-191
lines changed

7 files changed

+137
-191
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 118 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

src/JsonApiDotNetCore/Extensions/DbContextExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Threading.Tasks;
45
using JsonApiDotNetCore.Models;
56
using Microsoft.EntityFrameworkCore;
7+
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
68
using Microsoft.EntityFrameworkCore.Infrastructure;
79
using Microsoft.EntityFrameworkCore.Storage;
810

@@ -32,6 +34,7 @@ public static bool EntityIsTracked(this DbContext context, IIdentifiable entity)
3234
return GetTrackedEntity(context, entity) != null;
3335
}
3436

37+
3538
/// <summary>
3639
/// Determines whether or not EF is already tracking an entity of the same Type and Id
3740
/// and returns that entity.

src/JsonApiDotNetCore/Request/HasManyRelationshipPointers.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ namespace JsonApiDotNetCore.Request
3232
/// </summary>
3333
public class HasManyRelationshipPointers
3434
{
35-
private Dictionary<RelationshipAttribute, IList> _hasManyRelationships = new Dictionary<RelationshipAttribute, IList>();
35+
private readonly Dictionary<RelationshipAttribute, IList> _hasManyRelationships = new Dictionary<RelationshipAttribute, IList>();
3636

3737
/// <summary>
3838
/// Add the relationship to the list of relationships that should be

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr,
250250

251251
var convertedValue = TypeHelper.ConvertType(foreignKeyPropertyValue, foreignKeyProperty.PropertyType);
252252
foreignKeyProperty.SetValue(entity, convertedValue);
253-
_jsonApiContext.RelationshipsToUpdate[hasOneAttr] = convertedValue;
253+
//_jsonApiContext.RelationshipsToUpdate[hasOneAttr] = convertedValue;
254254
}
255255
}
256256

@@ -300,7 +300,7 @@ private object SetHasManyRelationship(object entity,
300300
var convertedCollection = TypeHelper.ConvertCollection(relatedResources, attr.Type);
301301

302302
attr.SetValue(entity, convertedCollection);
303-
_jsonApiContext.RelationshipsToUpdate[attr] = convertedCollection;
303+
//_jsonApiContext.RelationshipsToUpdate[attr] = convertedCollection; // WHAT exactly is the use of these two different collections..?
304304
_jsonApiContext.HasManyRelationshipPointers.Add(attr, convertedCollection);
305305
}
306306

0 commit comments

Comments
 (0)