Skip to content

Commit b53a03e

Browse files
committed
fix: error when exposing relationship information between layers in hooks, added comments to code
1 parent db857e5 commit b53a03e

File tree

6 files changed

+145
-53
lines changed

6 files changed

+145
-53
lines changed

src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public IResourceHookContainer<TEntity> GetResourceHookContainer<TEntity>(Resourc
8282
public IEnumerable LoadDbValues(PrincipalType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships)
8383
{
8484
var paths = relationships.Select(p => p.RelationshipPath).ToArray();
85-
var idType = GetIdentifierType(entityTypeForRepository);
85+
var idType = TypeHelper.GetIdentifierType(entityTypeForRepository);
8686
var parameterizedGetWhere = GetType()
8787
.GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance)
8888
.MakeGenericMethod(entityTypeForRepository, idType);
@@ -144,11 +144,6 @@ IHooksDiscovery GetHookDiscovery(Type entityType)
144144
return discovery;
145145
}
146146

147-
Type GetIdentifierType(Type entityType)
148-
{
149-
return entityType.GetProperty("Id").PropertyType;
150-
}
151-
152147
IEnumerable<TEntity> GetWhereAndInclude<TEntity, TId>(IEnumerable<TId> ids, string[] relationshipPaths) where TEntity : class, IIdentifiable<TId>
153148
{
154149
var repo = GetRepository<TEntity, TId>();

src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,20 @@ internal interface IHookExecutorHelper
4242
/// <summary>
4343
/// For a set of entities, loads current values from the database
4444
/// </summary>
45+
/// <param name="repositoryEntityType">type of the entities to be loaded</param>
46+
/// <param name="entities">The set of entities to load the db values for</param>
47+
/// <param name="hook">The hook in which the db values will be displayed.</param>
48+
/// <param name="relationships">Relationships that need to be included on entities.</param>
4549
IEnumerable LoadDbValues(Type repositoryEntityType, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships);
46-
bool ShouldLoadDbValues(Type containerEntityType, ResourceHook hook);
50+
51+
/// <summary>
52+
/// Checks if the display database values option is allowed for the targetd hook, and for
53+
/// a given resource of type <paramref name="entityType"/> checks if this hook is implemented and if the
54+
/// database values option is enabled.
55+
/// </summary>
56+
/// <returns><c>true</c>, if load db values was shoulded, <c>false</c> otherwise.</returns>
57+
/// <param name="entityType">Container entity type.</param>
58+
/// <param name="hook">Hook.</param>
59+
bool ShouldLoadDbValues(Type entityType, ResourceHook hook);
4760
}
4861
}

src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ public virtual IEnumerable<TEntity> BeforeCreate<TEntity>(IEnumerable<TEntity> e
7070
node.UpdateUnique(updated);
7171
node.Reassign(entities);
7272
}
73-
7473
FireNestedBeforeUpdateHooks(pipeline, _traversalHelper.CreateNextLayer(node));
7574
return entities;
7675
}
@@ -234,59 +233,105 @@ void RecursiveBeforeRead(ContextEntity contextEntity, List<string> relationshipC
234233
}
235234

236235
/// <summary>
237-
/// Fires the nested before hooks. For example consider the case when
238-
/// the owner of an article a1 (one-to-one) was updated from o1 to o2, where o2
239-
/// was already related to a2. Then, the BeforeUpdateRelationship should be
240-
/// fired for o2, and the BeforeImplicitUpdateRelationship hook should be fired for
241-
/// o2 and then too for a2.
236+
/// Fires the nested before hooks for entities in the current <paramref name="layer"/>
242237
/// </summary>
238+
/// <remarks>
239+
/// For example: consider the case when the owner of article1 (one-to-one)
240+
/// is being updated from owner_old to owner_new, where owner_new is currently already
241+
/// related to article2. Then, the following nested hooks need to be fired in the following order.
242+
/// First the BeforeUpdateRelationship should be for owner1, then the
243+
/// BeforeImplicitUpdateRelationship hook should be fired for
244+
/// owner2, and lastely the BeforeImplicitUpdateRelationship for article2.</remarks>
243245
void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer layer)
244246
{
245247
foreach (IEntityNode node in layer)
246248
{
247249
var nestedHookcontainer = _executorHelper.GetResourceHookContainer(node.EntityType, ResourceHook.BeforeUpdateRelationship);
248250
IEnumerable uniqueEntities = node.UniqueEntities;
249251
DependentType entityType = node.EntityType;
252+
Dictionary<RelationshipAttribute, IEnumerable> currenEntitiesGrouped;
253+
Dictionary<RelationshipAttribute, IEnumerable> currentEntitiesGroupedInverse;
250254

251-
// fire the BeforeUpdateRelationship hook for o2
255+
// fire the BeforeUpdateRelationship hook for owner_new
252256
if (nestedHookcontainer != null)
253257
{
254258
if (uniqueEntities.Cast<IIdentifiable>().Any())
255259
{
256260
var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray();
257261
var dbValues = LoadDbValues(entityType, uniqueEntities, ResourceHook.BeforeUpdateRelationship, relationships);
258262

259-
var dependentByPrevLayerRelationships = node.RelationshipsFromPreviousLayer.GetDependentEntities();
260-
var principalsByCurrentLayerRelationships = dependentByPrevLayerRelationships.ToDictionary(kvp => _graph.GetInverseRelationship(kvp.Key), kvp => kvp.Value);
261-
262-
var resourcesByRelationship = CreateRelationshipHelper(entityType, principalsByCurrentLayerRelationships, dbValues);
263+
/// these are the entities of the current node grouped by
264+
/// RelationshipAttributes that occured in the previous layer
265+
/// so it looks like { HasOneAttribute:owner => owner_new }.
266+
/// Note that in the BeforeUpdateRelationship hook of Person,
267+
/// we want want inverse relationship attribute:
268+
/// we now have the one pointing from article -> person, ]
269+
/// but we require the the one that points from person -> article
270+
currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities();
271+
currentEntitiesGroupedInverse = ReplaceKeysWithInverseRelationships(currenEntitiesGrouped);
272+
273+
var resourcesByRelationship = CreateRelationshipHelper(entityType, currentEntitiesGroupedInverse, dbValues);
263274
var allowedIds = CallHook(nestedHookcontainer, ResourceHook.BeforeUpdateRelationship, new object[] { GetIds(uniqueEntities), resourcesByRelationship, pipeline }).Cast<string>();
264275
var updated = GetAllowedEntities(uniqueEntities, allowedIds);
265276
node.UpdateUnique(updated);
266277
node.Reassign();
267278
}
268279
}
269280

270-
// fire the BeforeImplicitUpdateRelationship hook for o1
271-
var implicitPrincipalTargets = node.RelationshipsFromPreviousLayer.GetPrincipalEntities();
272-
if (pipeline != ResourcePipeline.Post && implicitPrincipalTargets.Any())
281+
/// Fire the BeforeImplicitUpdateRelationship hook for owner_old.
282+
/// Note: if the pipeline is Post it means we just created article1,
283+
/// which means we are sure that it isn't related to any other entities yet.
284+
if (pipeline != ResourcePipeline.Post)
273285
{
274-
// value in implicitPrincipalTargets is a1 here.
275-
// we need to load the current value in db, which is o1.
276-
// then we need to inverse the relationship attribute
277-
FireForAffectedImplicits(entityType, implicitPrincipalTargets, pipeline, uniqueEntities);
286+
/// To fire a hook for owner_old, we need to first get a reference to it.
287+
/// For this, we need to query the database for the HasOneAttribute:owner
288+
/// relationship of article1, which is referred to as the
289+
/// principal side of the HasOneAttribute:owner relationship.
290+
var principalEntities = node.RelationshipsFromPreviousLayer.GetPrincipalEntities();
291+
if (principalEntities.Any())
292+
{
293+
/// owner_old is loaded, which is an "implicitly affected entity"
294+
FireForAffectedImplicits(entityType, principalEntities, pipeline, uniqueEntities);
295+
}
278296
}
279297

280-
// fire the BeforeImplicitUpdateRelationship hook for a2
281-
var dependentEntities = node.RelationshipsFromPreviousLayer.GetDependentEntities();
282-
if (dependentEntities.Any())
298+
/// Fire the BeforeImplicitUpdateRelationship hook for article2
299+
/// For this, we need to query the database for the current owner
300+
/// relationship value of owner_new.
301+
currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities();
302+
if (currenEntitiesGrouped.Any())
283303
{
284-
(var implicitDependentTargets, var principalEntityType) = GetDependentImplicitsTargets(dependentEntities);
285-
FireForAffectedImplicits(principalEntityType, implicitDependentTargets, pipeline);
304+
/// dependentEntities is grouped by relationships from previous
305+
/// layer, ie { HasOneAttribute:owner => owner_new }. But
306+
/// to load article2 onto owner_new, we need to have the
307+
/// RelationshipAttribute from owner to article, which is the
308+
/// inverse of HasOneAttribute:owner
309+
currentEntitiesGroupedInverse = ReplaceKeysWithInverseRelationships(currenEntitiesGrouped);
310+
/// Note that currently in the JADNC implementation of hooks,
311+
/// the root layer is ALWAYS homogenous, so we safely assume
312+
/// that for every relationship to the previous layer, the
313+
/// principal type is the same.
314+
PrincipalType principalEntityType = currenEntitiesGrouped.First().Key.PrincipalType;
315+
FireForAffectedImplicits(principalEntityType, currentEntitiesGroupedInverse, pipeline);
286316
}
287317
}
288318
}
289319

320+
/// <summary>
321+
/// replaces the keys of the <paramref name="entitiesByRelationship"/> dictionary
322+
/// with its inverse relationship attribute.
323+
/// </summary>
324+
/// <param name="entitiesByRelationship">Entities grouped by relationship attribute</param>
325+
Dictionary<RelationshipAttribute, IEnumerable> ReplaceKeysWithInverseRelationships(Dictionary<RelationshipAttribute, IEnumerable> entitiesByRelationship)
326+
{
327+
/// when Article has one Owner (HasOneAttribute:owner) is set, there is no guarantee
328+
/// that the inverse attribute was also set (Owner has one Article: HasOneAttr:article).
329+
/// If it isn't, JADNC currently knows nothing about this relationship pointing back, and it
330+
/// currently cannot fire hooks for entities resolved through inverse relationships.
331+
var inversableRelationshipAttributes = entitiesByRelationship.Where(kvp => kvp.Key.InverseNavigation != null);
332+
return inversableRelationshipAttributes.ToDictionary(kvp => _graph.GetInverseRelationship(kvp.Key), kvp => kvp.Value);
333+
}
334+
290335
/// <summary>
291336
/// Given a source of entities, gets the implicitly affected entities
292337
/// from the database and calls the BeforeImplicitUpdateRelationship hook.
@@ -317,16 +362,6 @@ void ValidateHookResponse<T>(IEnumerable<T> returnedList, ResourcePipeline pipel
317362
}
318363
}
319364

320-
/// <summary>
321-
/// NOTE: in JADNC usage, the root layer is ALWAYS homogenous, so we can be sure that for every
322-
/// relationship to the previous layer, the principal type is the same.
323-
/// </summary>
324-
(Dictionary<RelationshipAttribute, IEnumerable>, PrincipalType) GetDependentImplicitsTargets(Dictionary<RelationshipAttribute, IEnumerable> dependentEntities)
325-
{
326-
PrincipalType principalType = dependentEntities.First().Key.PrincipalType;
327-
var byInverseRelationship = dependentEntities.Where(kvp => kvp.Key.InverseNavigation != null).ToDictionary(kvp => GetInverseRelationship(kvp.Key), kvp => kvp.Value);
328-
return (byInverseRelationship, principalType);
329-
}
330365

331366
/// <summary>
332367
/// A helper method to call a hook on <paramref name="container"/> reflectively.
@@ -389,26 +424,46 @@ HashSet<IIdentifiable> GetAllowedEntities(IEnumerable source, IEnumerable<string
389424
return new HashSet<IIdentifiable>(source.Cast<IIdentifiable>().Where(ue => allowedIds.Contains(ue.StringId)));
390425
}
391426

427+
392428
/// <summary>
393-
/// Gets the inverse <see cref="RelationshipAttribute"/> for <paramref name="attribute"/>
429+
/// given the set of <paramref name="uniqueEntities"/>, it will load all the
430+
/// values from the database of these entites.
394431
/// </summary>
395-
RelationshipAttribute GetInverseRelationship(RelationshipAttribute attribute)
432+
/// <returns>The db values.</returns>
433+
/// <param name="entityType">type of the entities to be loaded</param>
434+
/// <param name="uniqueEntities">The set of entities to load the db values for</param>
435+
/// <param name="targetHook">The hook in which the db values will be displayed.</param>
436+
/// <param name="relationshipsToNextLayer">Relationships from <paramref name="entityType"/> to the next layer:
437+
/// this indicates which relationships will be included on <paramref name="uniqueEntities"/>.</param>
438+
IEnumerable LoadDbValues(Type entityType, IEnumerable uniqueEntities, ResourceHook targetHook, RelationshipAttribute[] relationshipsToNextLayer)
396439
{
397-
return _graph.GetInverseRelationship(attribute);
440+
/// We only need to load database values if the target hook of this hook execution
441+
/// cycle is compatible with displaying database values and has this option enabled.
442+
if (!_executorHelper.ShouldLoadDbValues(entityType, targetHook)) return null;
443+
return _executorHelper.LoadDbValues(entityType, uniqueEntities, targetHook, relationshipsToNextLayer);
398444
}
399445

400-
IEnumerable LoadDbValues(Type containerEntityType, IEnumerable uniqueEntities, ResourceHook targetHook, RelationshipAttribute[] relationshipsToNextLayer)
401-
{
402-
if (!_executorHelper.ShouldLoadDbValues(containerEntityType, targetHook)) return null;
403-
return _executorHelper.LoadDbValues(containerEntityType, uniqueEntities, targetHook, relationshipsToNextLayer);
404-
}
405446

447+
/// <summary>
448+
/// Fires the AfterUpdateRelationship hook
449+
/// </summary>
406450
void FireAfterUpdateRelationship(IResourceHookContainer container, IEntityNode node, ResourcePipeline pipeline)
407451
{
408-
var resourcesByRelationship = CreateRelationshipHelper(node.EntityType, node.RelationshipsFromPreviousLayer.GetDependentEntities());
452+
453+
Dictionary<RelationshipAttribute, IEnumerable> currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities();
454+
/// the relationships attributes in currenEntitiesGrouped will be pointing from a
455+
/// resource in the previouslayer to a resource in the current (nested) layer.
456+
/// For the nested hook we need to replace these attributes with their inverse.
457+
/// See the FireNestedBeforeUpdateHooks method for a more detailed example.
458+
var resourcesByRelationship = CreateRelationshipHelper(node.EntityType, ReplaceKeysWithInverseRelationships(currenEntitiesGrouped));
409459
CallHook(container, ResourceHook.AfterUpdateRelationship, new object[] { resourcesByRelationship, pipeline });
410460
}
411461

462+
/// <summary>
463+
/// Returns a list of StringIds from a list of IIdentifiables (<paramref name="entities"/>).
464+
/// </summary>
465+
/// <returns>The ids.</returns>
466+
/// <param name="entities">iidentifiable entities.</param>
412467
HashSet<string> GetIds(IEnumerable entities)
413468
{
414469
return new HashSet<string>(entities.Cast<IIdentifiable>().Select(e => e.StringId));

src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public EntityChildLayer CreateNextLayer(IEnumerable<IEntityNode> nodes)
9191
var nextNodes = principalsGrouped.Select(entry =>
9292
{
9393
var nextNodeType = entry.Key;
94+
RegisterRelationshipProxies(nextNodeType);
95+
9496
var populatedRelationships = new List<RelationshipProxy>();
9597
var relationshipsToPreviousLayer = entry.Value.Select(grouped =>
9698
{
@@ -99,7 +101,6 @@ public EntityChildLayer CreateNextLayer(IEnumerable<IEntityNode> nodes)
99101
return CreateRelationshipGroupInstance(nextNodeType, proxy, grouped.Value, dependents[proxy]);
100102
}).ToList();
101103

102-
RegisterRelationshipProxies(nextNodeType);
103104
return CreateNodeInstance(nextNodeType, populatedRelationships.ToArray(), relationshipsToPreviousLayer);
104105
}).ToList();
105106

@@ -201,7 +202,8 @@ void RegisterRelationshipProxies(DependentType type)
201202
if (!RelationshipProxies.TryGetValue(attr, out RelationshipProxy proxies))
202203
{
203204
DependentType dependentType = GetDependentTypeFromRelationship(attr);
204-
var isContextRelation = (_context.RelationshipsToUpdate?.ContainsKey(attr)) != null;
205+
bool isContextRelation = false;
206+
if (_context.RelationshipsToUpdate != null) isContextRelation = _context.RelationshipsToUpdate.ContainsKey(attr);
205207
var proxy = new RelationshipProxy(attr, dependentType, isContextRelation);
206208
RelationshipProxies[attr] = proxy;
207209
}

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,5 +181,23 @@ public static Type GetListInnerType(IEnumerable list)
181181
{
182182
return list.GetType().GetGenericArguments()[0];
183183
}
184+
185+
/// <summary>
186+
/// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable
187+
/// </summary>
188+
public static Type GetIdentifierType(Type entityType)
189+
{
190+
var property = entityType.GetProperty("Id");
191+
if (property == null) throw new ArgumentException("Type does not have a property Id");
192+
return entityType.GetProperty("Id").PropertyType;
193+
}
194+
195+
/// <summary>
196+
/// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable
197+
/// </summary>
198+
public static Type GetIdentifierType<T>() where T : IIdentifiable
199+
{
200+
return typeof(T).GetProperty("Id").PropertyType;
201+
}
184202
}
185203
}

0 commit comments

Comments
 (0)