Skip to content

Commit 457e93d

Browse files
committed
chore: comments
1 parent 9139852 commit 457e93d

File tree

5 files changed

+94
-29
lines changed

5 files changed

+94
-29
lines changed

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public DefaultEntityRepository(
6262
_resourceDefinition = resourceDefinition;
6363
}
6464

65-
public DefaultEntityRepository(
65+
public
66+
DefaultEntityRepository(
6667
ILoggerFactory loggerFactory,
6768
IJsonApiContext jsonApiContext,
6869
IDbContextResolver contextResolver,
@@ -171,6 +172,13 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
171172
/// <summary>
172173
/// Loads the inverse relationships to prevent foreign key constraints from being violated
173174
/// to support implicit removes, see https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/502.
175+
///
176+
/// example:
177+
/// person.todoItems = [t1,t2] is updated to [t3, t4]. If t3, and/or t4 was
178+
/// already related to a other person, and these persons are NOT loaded in to the
179+
/// db context, then the query may cause a foreign key constraint. Loading
180+
/// these "inverse relationships" into the DB context ensures EF core to take
181+
/// this into account.
174182
/// </summary>
175183
private void LoadInverseRelationships(object trackedRelationshipValue, RelationshipAttribute relationshipAttr)
176184
{
@@ -181,14 +189,15 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
181189
if (IsHasOneRelationship(hasOneAttr.InverseNavigation, trackedRelationshipValue.GetType()))
182190
{
183191
relationEntry.Reference(hasOneAttr.InverseNavigation).Load();
184-
} else
192+
}
193+
else
185194
{
186195
relationEntry.Collection(hasOneAttr.InverseNavigation).Load();
187196
}
188197
}
189198
else if (relationshipAttr is HasManyAttribute hasManyAttr && !(relationshipAttr is HasManyThroughAttribute))
190199
{
191-
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
200+
foreach (IIdentifiable relationshipValue in (IList)trackedRelationshipValue)
192201
{
193202
_context.Entry(relationshipValue).Reference(hasManyAttr.InverseNavigation).Load();
194203
}
@@ -198,9 +207,18 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
198207

199208
private bool IsHasOneRelationship(string internalRelationshipName, Type type)
200209
{
201-
var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.Single(r => r.InternalRelationshipName == internalRelationshipName);
202-
if (relationshipAttr is HasOneAttribute) return true;
203-
return false;
210+
var relationshipAttr = _jsonApiContext.ResourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
211+
if (relationshipAttr != null)
212+
{
213+
if (relationshipAttr is HasOneAttribute) return true;
214+
return false;
215+
}
216+
else
217+
{
218+
// relationshipAttr is null when there is not put a [RelationshipAttribute] on the inverse navigation property.
219+
// In this case we use relfection to figure out what kind of relationship is pointing back.
220+
return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)));
221+
}
204222
}
205223

206224

@@ -247,31 +265,39 @@ public virtual async Task<TEntity> UpdateAsync(TId id, TEntity updatedEntity)
247265
/// <inheritdoc />
248266
public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
249267
{
250-
var oldEntity = await GetAsync(updatedEntity.Id);
251-
if (oldEntity == null)
268+
var databaseEntity = await GetAsync(updatedEntity.Id);
269+
if (databaseEntity == null)
252270
return null;
253271

254272
foreach (var attr in _jsonApiContext.AttributesToUpdate.Keys)
255-
attr.SetValue(oldEntity, attr.GetValue(updatedEntity));
273+
attr.SetValue(databaseEntity, attr.GetValue(updatedEntity));
256274

257275
foreach (var relationshipAttr in _jsonApiContext.RelationshipsToUpdate?.Keys)
258276
{
259-
LoadCurrentRelationships(oldEntity, relationshipAttr);
260-
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
277+
/// loads databasePerson.todoItems
278+
LoadCurrentRelationships(databaseEntity, relationshipAttr);
279+
/// trackedRelationshipValue is either equal to updatedPerson.todoItems
280+
/// or replaced with the same set of todoItems from the EF Core change tracker,
281+
/// if they were already tracked
282+
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
283+
/// loads into the db context any persons currently related
284+
/// to the todoItems in trackedRelationshipValue
261285
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
262-
AssignRelationshipValue(oldEntity, trackedRelationshipValue, relationshipAttr);
286+
/// assigns the updated relationship to the database entity
287+
AssignRelationshipValue(databaseEntity, trackedRelationshipValue, relationshipAttr);
263288
}
264289

265290
await _context.SaveChangesAsync();
266-
return oldEntity;
291+
return databaseEntity;
267292
}
268293

269294

270295
/// <summary>
271296
/// Responsible for getting the relationship value for a given relationship
272297
/// attribute of a given entity. It ensures that the relationship value
273298
/// that it returns is attached to the database without reattaching duplicates instances
274-
/// to the change tracker.
299+
/// to the change tracker. It does so by checking if there already are
300+
/// instances of the to-be-attached entities in the change tracker.
275301
/// </summary>
276302
private object GetTrackedRelationshipValue(RelationshipAttribute relationshipAttr, TEntity entity, out bool wasAlreadyAttached)
277303
{
@@ -436,9 +462,16 @@ public async Task<IReadOnlyList<TEntity>> ToListAsync(IQueryable<TEntity> entiti
436462

437463
/// <summary>
438464
/// Before assigning new relationship values (UpdateAsync), we need to
439-
/// attach the current relationship state to the dbcontext, else
465+
/// attach the current database values of the relationship to the dbcontext, else
440466
/// it will not perform a complete-replace which is required for
441467
/// one-to-many and many-to-many.
468+
/// <para />
469+
/// For example: a person `p1` has 2 todoitems: `t1` and `t2`.
470+
/// If we want to update this todoitem set to `t3` and `t4`, simply assigning
471+
/// `p1.todoItems = [t3, t4]` will result in EF Core adding them to the set,
472+
/// resulting in `[t1 ... t4]`. Instead, we should first include `[t1, t2]`,
473+
/// after which the reassignment `p1.todoItems = [t3, t4]` will actually
474+
/// make EF Core perform a complete replace. This method does the loading of `[t1, t2]`.
442475
/// </summary>
443476
protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute relationshipAttribute)
444477
{
@@ -454,21 +487,18 @@ protected void LoadCurrentRelationships(TEntity oldEntity, RelationshipAttribute
454487
}
455488

456489
/// <summary>
457-
/// assigns relationships that were set in the request to the target entity of the request
458-
/// todo: partially remove dependency on IJsonApiContext here: it is fine to
459-
/// retrieve from the context WHICH relationships to update, but the actual
460-
/// values should not come from the context.
490+
/// Assigns the <paramref name="relationshipValue"/> to <paramref name="targetEntity"/>
461491
/// </summary>
462-
private void AssignRelationshipValue(TEntity oldEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
492+
private void AssignRelationshipValue(TEntity targetEntity, object relationshipValue, RelationshipAttribute relationshipAttribute)
463493
{
464494
if (relationshipAttribute is HasManyThroughAttribute throughAttribute)
465495
{
466496
// todo: this logic should be put in the HasManyThrough attribute
467-
AssignHasManyThrough(oldEntity, throughAttribute, (IList)relationshipValue);
497+
AssignHasManyThrough(targetEntity, throughAttribute, (IList)relationshipValue);
468498
}
469499
else
470500
{
471-
relationshipAttribute.SetValue(oldEntity, relationshipValue);
501+
relationshipAttribute.SetValue(targetEntity, relationshipValue);
472502
}
473503
}
474504

src/JsonApiDotNetCore/Extensions/TypeExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@ namespace JsonApiDotNetCore.Extensions
88
{
99
internal static class TypeExtensions
1010
{
11+
public static void AddRange<T>(this IList list, IEnumerable<T> items)
12+
{
13+
if (list == null) throw new ArgumentNullException(nameof(list));
14+
if (items == null) throw new ArgumentNullException(nameof(items));
15+
16+
if (list is List<T>)
17+
{
18+
((List<T>)list).AddRange(items);
19+
}
20+
else
21+
{
22+
foreach (var item in items)
23+
{
24+
list.Add(item);
25+
}
26+
}
27+
}
28+
29+
30+
/// <summary>
31+
/// Extension to use the LINQ cast method in a non-generic way:
32+
/// <code>
33+
/// Type targetType = typeof(TEntity)
34+
/// ((IList)myList).Cast(targetType).
35+
/// </code>
36+
/// </summary>
1137
public static IEnumerable Cast(this IEnumerable source, Type type)
1238
{
1339
if (source == null) throw new ArgumentNullException(nameof(source));

src/JsonApiDotNetCore/Internal/InverseRelationships.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ namespace JsonApiDotNetCore.Internal
1818
/// </summary>
1919
public interface IInverseRelationships
2020
{
21+
/// <summary>
22+
/// This method is called upon startup by JsonApiDotNetCore. It should
23+
/// deal with resolving the inverse relationships.
24+
/// </summary>
2125
void Resolve();
2226
}
2327

28+
/// <inheritdoc />
2429
public class InverseRelationships : IInverseRelationships
2530
{
2631
private readonly ResourceGraph _graph;
@@ -32,6 +37,7 @@ public InverseRelationships(IResourceGraph graph, IDbContextResolver resolver =
3237
_resolver = resolver;
3338
}
3439

40+
/// <inheritdoc />
3541
public void Resolve()
3642
{
3743
if (EntityFrameworkCoreIsEnabled())

src/JsonApiDotNetCore/Models/RelationshipAttribute.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ protected RelationshipAttribute(string publicName, Link documentLinks, bool canI
1616

1717
public string PublicRelationshipName { get; internal set; }
1818
public string InternalRelationshipName { get; internal set; }
19-
2019
public string InverseNavigation { get; internal set; }
2120

2221
/// <summary>

src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ private object SetEntityAttributes(
159159
/// through the decoupling IJsonApiContext), we now no longer need to
160160
/// store the updated relationship values in this property. For now
161161
/// just assigning null as value, will remove this property later as a whole.
162+
/// see #512
162163
_jsonApiContext.AttributesToUpdate[attr] = null;
163164
}
164165
}
@@ -224,14 +225,14 @@ private object SetHasOneRelationship(object entity,
224225
SetHasOneNavigationPropertyValue(entity, attr, rio, included);
225226

226227
// recursive call ...
227-
if(included != null)
228+
if (included != null)
228229
{
229230
var navigationPropertyValue = attr.GetValue(entity);
230231
var resourceGraphEntity = _jsonApiContext.ResourceGraph.GetContextEntity(attr.Type);
231-
if(navigationPropertyValue != null && resourceGraphEntity != null)
232+
if (navigationPropertyValue != null && resourceGraphEntity != null)
232233
{
233234
var includedResource = included.SingleOrDefault(r => r.Type == rio.Type && r.Id == rio.Id);
234-
if(includedResource != null)
235+
if (includedResource != null)
235236
SetRelationships(navigationPropertyValue, resourceGraphEntity, includedResource.Relationships, included);
236237
}
237238
}
@@ -257,7 +258,8 @@ private void SetHasOneForeignKeyValue(object entity, HasOneAttribute hasOneAttr,
257258
/// through the decoupling IJsonApiContext), we now no longer need to
258259
/// store the updated relationship values in this property. For now
259260
/// just assigning null as value, will remove this property later as a whole.
260-
if (convertedValue == null ) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
261+
/// see #512
262+
if (convertedValue == null) _jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
261263
}
262264
}
263265

@@ -281,6 +283,7 @@ private void SetHasOneNavigationPropertyValue(object entity, HasOneAttribute has
281283
/// through the decoupling IJsonApiContext), we now no longer need to
282284
/// store the updated relationship values in this property. For now
283285
/// just assigning null as value, will remove this property later as a whole.
286+
/// see #512
284287
_jsonApiContext.HasOneRelationshipPointers.Add(hasOneAttr, null);
285288
}
286289
}
@@ -296,7 +299,7 @@ private object SetHasManyRelationship(object entity,
296299

297300
if (relationships.TryGetValue(relationshipName, out RelationshipData relationshipData))
298301
{
299-
if(relationshipData.IsHasMany == false || relationshipData.ManyData == null)
302+
if (relationshipData.IsHasMany == false || relationshipData.ManyData == null)
300303
return entity;
301304

302305
var relatedResources = relationshipData.ManyData.Select(r =>
@@ -312,7 +315,8 @@ private object SetHasManyRelationship(object entity,
312315
/// through the decoupling IJsonApiContext), we now no longer need to
313316
/// store the updated relationship values in this property. For now
314317
/// just assigning null as value, will remove this property later as a whole.
315-
_jsonApiContext.HasManyRelationshipPointers.Add(attr, null);
318+
/// see #512
319+
_jsonApiContext.HasManyRelationshipPointers.Add(attr, null);
316320
}
317321

318322
return entity;

0 commit comments

Comments
 (0)