Skip to content

Commit 9f7550c

Browse files
committed
feat: decoupled repository from JsonApiContext with respect to updating entities
1 parent f0d5924 commit 9f7550c

File tree

1 file changed

+121
-116
lines changed

1 file changed

+121
-116
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

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

Comments
 (0)