diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs index b333e88536..d7147123f6 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs @@ -58,15 +58,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(t => t.ParentTodoItem) .HasForeignKey(t => t.ParentTodoItemId); - modelBuilder.Entity() - .HasOne(p => p.Passport) - .WithOne(p => p.Person) - .HasForeignKey(p => p.PassportId); - modelBuilder.Entity() .HasOne(p => p.Person) .WithOne(p => p.Passport) - .HasForeignKey(p => p.PassportId); + .HasForeignKey(p => p.PassportId) + .OnDelete(DeleteBehavior.SetNull); modelBuilder.Entity() .HasOne(p => p.ToOnePerson) diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResourceBase.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs similarity index 76% rename from src/Examples/JsonApiDotNetCoreExample/Resources/LockableResourceBase.cs rename to src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs index 7e2ce4f658..7ad9659f18 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResourceBase.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/LockableResource.cs @@ -7,9 +7,9 @@ namespace JsonApiDotNetCoreExample.Resources { - public abstract class LockableResourceBase : ResourceDefinition where T : class, IIsLockable, IIdentifiable + public abstract class LockableResource : ResourceDefinition where T : class, IIsLockable, IIdentifiable { - protected LockableResourceBase(IResourceGraph graph) : base(graph) { } + protected LockableResource(IResourceGraph graph) : base(graph) { } protected void DisallowLocked(IEnumerable entities) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs index 032c9acb4b..4887414e73 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs @@ -6,25 +6,19 @@ namespace JsonApiDotNetCoreExample.Resources { - public class PersonResource : LockableResourceBase + public class PersonResource : LockableResource { public PersonResource(IResourceGraph graph) : base(graph) { } - public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { - BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline); + BeforeImplicitUpdateRelationship(entitiesByRelationship, pipeline); return ids; } - //[LoadDatabaseValues(true)] - //public override IEnumerable BeforeUpdate(IResourceDiff entityDiff, ResourcePipeline pipeline) - //{ - // return entityDiff.Entities; - //} - - public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) + public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { - resourcesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value)); + entitiesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value)); } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs index 02a7ba6f15..cfba9855d3 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/TodoResource.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreExample.Resources { - public class TodoResource : LockableResourceBase + public class TodoResource : LockableResource { public TodoResource(IResourceGraph graph) : base(graph) { } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/EntityDiffs.cs b/src/JsonApiDotNetCore/Hooks/Execution/EntityDiffs.cs index f0379636b5..fb30c26c7d 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/EntityDiffs.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/EntityDiffs.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; @@ -12,40 +13,38 @@ namespace JsonApiDotNetCore.Hooks /// Contains the resources from the request and the corresponding database values. /// /// Also contains information about updated relationships through - /// implementation of IRelationshipsDictionary> + /// implementation of IRelationshipsDictionary> /// - public interface IEntityDiff : IRelationshipsDictionary, IEnumerable> where TEntity : class, IIdentifiable + public interface IEntityDiffs : IEnumerable> where TResource : class, IIdentifiable { /// /// The database values of the resources affected by the request. /// - HashSet DatabaseValues { get; } + HashSet DatabaseValues { get; } /// /// The resources that were affected by the request. /// - HashSet Entities { get; } + EntityHashSet Entities { get; } + } /// - public class EntityDiffs : IEntityDiff where TEntity : class, IIdentifiable + public class EntityDiffs : IEntityDiffs where TResource : class, IIdentifiable { /// - public HashSet DatabaseValues { get => _databaseValues ?? ThrowNoDbValuesError(); } - private readonly HashSet _databaseValues; - private readonly bool _databaseValuesLoaded; - - /// - public HashSet Entities { get; private set; } + public HashSet DatabaseValues { get => _databaseValues ?? ThrowNoDbValuesError(); } /// - public RelationshipsDictionary AffectedRelationships { get; private set; } + public EntityHashSet Entities { get; private set; } - public EntityDiffs(HashSet requestEntities, - HashSet databaseEntities, - Dictionary> relationships) + private readonly HashSet _databaseValues; + private readonly bool _databaseValuesLoaded; + + public EntityDiffs(HashSet requestEntities, + HashSet databaseEntities, + Dictionary> relationships) { - Entities = requestEntities; - AffectedRelationships = new RelationshipsDictionary(relationships); + Entities = new EntityHashSet(requestEntities, relationships); _databaseValues = databaseEntities; _databaseValuesLoaded |= _databaseValues != null; } @@ -55,39 +54,27 @@ public EntityDiffs(HashSet requestEntities, /// internal EntityDiffs(IEnumerable requestEntities, IEnumerable databaseEntities, - Dictionary relationships) - : this((HashSet)requestEntities, (HashSet)databaseEntities, TypeHelper.ConvertRelationshipDictionary(relationships)) { } - + Dictionary relationships) + : this((HashSet)requestEntities, (HashSet)databaseEntities, TypeHelper.ConvertRelationshipDictionary(relationships)) { } - /// - public Dictionary> GetByRelationship() where TPrincipalResource : class, IIdentifiable - { - return GetByRelationship(typeof(TPrincipalResource)); - } - - /// - public Dictionary> GetByRelationship(Type principalType) - { - return AffectedRelationships.GetByRelationship(principalType); - } /// - public IEnumerator> GetEnumerator() + public IEnumerator> GetEnumerator() { if (!_databaseValuesLoaded) ThrowNoDbValuesError(); foreach (var entity in Entities) { - TEntity currentValueInDatabase = null; + TResource currentValueInDatabase = null; currentValueInDatabase = _databaseValues.Single(e => entity.StringId == e.StringId); - yield return new EntityDiffPair(entity, currentValueInDatabase); + yield return new EntityDiffPair(entity, currentValueInDatabase); } } /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private HashSet ThrowNoDbValuesError() + private HashSet ThrowNoDbValuesError() { throw new MemberAccessException("Cannot access database entities if the LoadDatabaseValues option is set to false"); } @@ -97,9 +84,9 @@ private HashSet ThrowNoDbValuesError() /// A wrapper that contains an entity that is affected by the request, /// matched to its current database value /// - public class EntityDiffPair where TEntity : class, IIdentifiable + public class EntityDiffPair where TResource : class, IIdentifiable { - public EntityDiffPair(TEntity entity, TEntity databaseValue) + public EntityDiffPair(TResource entity, TResource databaseValue) { Entity = entity; DatabaseValue = databaseValue; @@ -108,10 +95,10 @@ public EntityDiffPair(TEntity entity, TEntity databaseValue) /// /// The resource from the request matching the resource from the database. /// - public TEntity Entity { get; private set; } + public TResource Entity { get; private set; } /// /// The resource from the database matching the resource from the request. /// - public TEntity DatabaseValue { get; private set; } + public TResource DatabaseValue { get; private set; } } } diff --git a/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs b/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs index 98d7393796..8a97705c39 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs @@ -3,6 +3,8 @@ using System.Collections; using JsonApiDotNetCore.Internal; using System; +using System.Collections.ObjectModel; +using System.Collections.Immutable; namespace JsonApiDotNetCore.Hooks { @@ -12,7 +14,7 @@ namespace JsonApiDotNetCore.Hooks /// Also contains information about updated relationships through /// implementation of IAffectedRelationshipsDictionary> /// - public interface IEntityHashSet : IRelationshipsDictionary, IEnumerable where TResource : class, IIdentifiable { } + public interface IEntityHashSet : IByAffectedRelationships, IReadOnlyCollection where TResource : class, IIdentifiable { } /// /// Implementation of IResourceHashSet{TResource}. @@ -24,13 +26,16 @@ public interface IEntityHashSet : IRelationshipsDictionary /// public class EntityHashSet : HashSet, IEntityHashSet where TResource : class, IIdentifiable { + + /// - public RelationshipsDictionary AffectedRelationships { get; private set; } + public Dictionary> AffectedRelationships { get => _relationships; } + private readonly RelationshipsDictionary _relationships; public EntityHashSet(HashSet entities, Dictionary> relationships) : base(entities) { - AffectedRelationships = new RelationshipsDictionary(relationships); + _relationships = new RelationshipsDictionary(relationships); } /// @@ -44,13 +49,13 @@ internal EntityHashSet(IEnumerable entities, /// public Dictionary> GetByRelationship(Type principalType) { - return AffectedRelationships.GetByRelationship(principalType); + return _relationships.GetByRelationship(principalType); } /// - public Dictionary> GetByRelationship() where TPrincipalResource : class, IIdentifiable + public Dictionary> GetByRelationship() where TRelatedResource : class, IIdentifiable { - return GetByRelationship(); + return GetByRelationship(typeof(TRelatedResource)); } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs index 6a2c2767bf..df7f4daa0c 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs @@ -82,7 +82,7 @@ public IResourceHookContainer GetResourceHookContainer(Resourc public IEnumerable LoadDbValues(PrincipalType entityTypeForRepository, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships) { var paths = relationships.Select(p => p.RelationshipPath).ToArray(); - var idType = GetIdentifierType(entityTypeForRepository); + var idType = TypeHelper.GetIdentifierType(entityTypeForRepository); var parameterizedGetWhere = GetType() .GetMethod(nameof(GetWhereAndInclude), BindingFlags.NonPublic | BindingFlags.Instance) .MakeGenericMethod(entityTypeForRepository, idType); @@ -144,11 +144,6 @@ IHooksDiscovery GetHookDiscovery(Type entityType) return discovery; } - Type GetIdentifierType(Type entityType) - { - return entityType.GetProperty("Id").PropertyType; - } - IEnumerable GetWhereAndInclude(IEnumerable ids, string[] relationshipPaths) where TEntity : class, IIdentifiable { var repo = GetRepository(); diff --git a/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs b/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs index 63b3621a34..98de544f0a 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/IHookExecutorHelper.cs @@ -42,7 +42,20 @@ internal interface IHookExecutorHelper /// /// For a set of entities, loads current values from the database /// + /// type of the entities to be loaded + /// The set of entities to load the db values for + /// The hook in which the db values will be displayed. + /// Relationships that need to be included on entities. IEnumerable LoadDbValues(Type repositoryEntityType, IEnumerable entities, ResourceHook hook, params RelationshipAttribute[] relationships); - bool ShouldLoadDbValues(Type containerEntityType, ResourceHook hook); + + /// + /// Checks if the display database values option is allowed for the targetd hook, and for + /// a given resource of type checks if this hook is implemented and if the + /// database values option is enabled. + /// + /// true, if load db values was shoulded, false otherwise. + /// Container entity type. + /// Hook. + bool ShouldLoadDbValues(Type entityType, ResourceHook hook); } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs b/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs index 77248f5169..b6125b40d3 100644 --- a/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs +++ b/src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs @@ -1,39 +1,54 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Hooks { + /// + /// A dummy interface used internally by the hook executor. + /// public interface IRelationshipsDictionary { } /// /// An interface that is implemented to expose a relationship dictionary on another class. /// - public interface IExposeRelationshipsDictionary : IRelationshipsDictionary where TDependentResource : class, IIdentifiable + public interface IByAffectedRelationships : + IRelationshipGetters where TDependentResource : class, IIdentifiable { + /// todo: expose getters that behave something like this: + /// relationshipDictionary.GetAffected( entity => entity.NavigationProperty ). + /// see https://stackoverflow.com/a/17116267/4441216 + /// /// Gets a dictionary of affected resources grouped by affected relationships. /// - RelationshipsDictionary AffectedRelationships { get; } + Dictionary> AffectedRelationships { get; } } /// /// A helper class that provides insights in which relationships have been updated for which entities. /// - public interface IRelationshipsDictionary : IRelationshipsDictionary where TDependentResource : class, IIdentifiable + public interface IRelationshipsDictionary : + IRelationshipGetters, + IReadOnlyDictionary>, + IRelationshipsDictionary where TDependentResource : class, IIdentifiable { } + + /// + /// A helper class that provides insights in which relationships have been updated for which entities. + /// + public interface IRelationshipGetters where TResource : class, IIdentifiable { /// /// Gets a dictionary of all entities that have an affected relationship to type /// - Dictionary> GetByRelationship() where TPrincipalResource : class, IIdentifiable; + Dictionary> GetByRelationship() where TRelatedResource : class, IIdentifiable; /// /// Gets a dictionary of all entities that have an affected relationship to type /// - Dictionary> GetByRelationship(Type principalType); + Dictionary> GetByRelationship(Type relatedResourceType); } /// @@ -42,37 +57,32 @@ public interface IRelationshipsDictionary : IRelationshipsDi /// It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TDependentResource}} dictionary /// with the two helper methods defined on IAffectedRelationships{TDependentResource}. /// - public class RelationshipsDictionary : ReadOnlyDictionary>, IRelationshipsDictionary where TDependentResource : class, IIdentifiable + public class RelationshipsDictionary : + Dictionary>, + IRelationshipsDictionary where TResource : class, IIdentifiable { /// - /// a dictionary with affected relationships as keys and values being the corresponding resources - /// that were affected + /// Initializes a new instance of the class. /// - private readonly Dictionary> _groups; - - /// - public RelationshipsDictionary(Dictionary> relationships) : base(relationships) - { - _groups = relationships; - } + /// Relationships. + public RelationshipsDictionary(Dictionary> relationships) : base(relationships) { } /// /// Used internally by the ResourceHookExecutor to make live a bit easier with generics /// internal RelationshipsDictionary(Dictionary relationships) - : this(TypeHelper.ConvertRelationshipDictionary(relationships)) { } - + : this(TypeHelper.ConvertRelationshipDictionary(relationships)) { } /// - public Dictionary> GetByRelationship() where TPrincipalResource : class, IIdentifiable + public Dictionary> GetByRelationship() where TRelatedResource : class, IIdentifiable { - return GetByRelationship(typeof(TPrincipalResource)); + return GetByRelationship(typeof(TRelatedResource)); } /// - public Dictionary> GetByRelationship(Type principalType) + public Dictionary> GetByRelationship(Type relatedType) { - return this.Where(p => p.Key.PrincipalType == principalType).ToDictionary(p => p.Key, p => p.Value); + return this.Where(p => p.Key.DependentType == relatedType).ToDictionary(p => p.Key, p => p.Value); } } } diff --git a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs index cadb96a4fa..75b52cbddd 100644 --- a/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs +++ b/src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs @@ -77,7 +77,7 @@ public interface IBeforeHooks where TResource : class, IIdentifiable /// The transformed entity set /// The entity diff. /// An enum indicating from where the hook was triggered. - IEnumerable BeforeUpdate(IEntityDiff entityDiff, ResourcePipeline pipeline); + IEnumerable BeforeUpdate(IEntityDiffs entityDiff, ResourcePipeline pipeline); /// /// Implement this hook to run custom logic in the diff --git a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs index 7fccbbba7f..30bb51560d 100644 --- a/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs +++ b/src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs @@ -59,7 +59,6 @@ public virtual IEnumerable BeforeUpdate(IEnumerable e return entities; } - /// public virtual IEnumerable BeforeCreate(IEnumerable entities, ResourcePipeline pipeline) where TEntity : class, IIdentifiable { @@ -70,7 +69,6 @@ public virtual IEnumerable BeforeCreate(IEnumerable e node.UpdateUnique(updated); node.Reassign(entities); } - FireNestedBeforeUpdateHooks(pipeline, _traversalHelper.CreateNextLayer(node)); return entities; } @@ -89,7 +87,11 @@ public virtual IEnumerable BeforeDelete(IEnumerable e node.Reassign(entities); } - foreach (var entry in node.PrincipalsToNextLayerByType()) + /// If we're deleting an article, we're implicitly affected any owners related to it. + /// Here we're loading all relations onto the to-be-deleted article + /// if for that relation the BeforeImplicitUpdateHook is implemented, + /// and this hook is then executed + foreach (var entry in node.PrincipalsToNextLayerByRelationships()) { var dependentType = entry.Key; var implicitTargets = entry.Value; @@ -234,12 +236,15 @@ void RecursiveBeforeRead(ContextEntity contextEntity, List relationshipC } /// - /// Fires the nested before hooks. For example consider the case when - /// the owner of an article a1 (one-to-one) was updated from o1 to o2, where o2 - /// was already related to a2. Then, the BeforeUpdateRelationship should be - /// fired for o2, and the BeforeImplicitUpdateRelationship hook should be fired for - /// o2 and then too for a2. + /// Fires the nested before hooks for entities in the current /// + /// + /// For example: consider the case when the owner of article1 (one-to-one) + /// is being updated from owner_old to owner_new, where owner_new is currently already + /// related to article2. Then, the following nested hooks need to be fired in the following order. + /// First the BeforeUpdateRelationship should be for owner1, then the + /// BeforeImplicitUpdateRelationship hook should be fired for + /// owner2, and lastely the BeforeImplicitUpdateRelationship for article2. void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer layer) { foreach (IEntityNode node in layer) @@ -247,15 +252,28 @@ void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer lay var nestedHookcontainer = _executorHelper.GetResourceHookContainer(node.EntityType, ResourceHook.BeforeUpdateRelationship); IEnumerable uniqueEntities = node.UniqueEntities; DependentType entityType = node.EntityType; + Dictionary currenEntitiesGrouped; + Dictionary currentEntitiesGroupedInverse; - // fire the BeforeUpdateRelationship hook for o1 + // fire the BeforeUpdateRelationship hook for owner_new if (nestedHookcontainer != null) { if (uniqueEntities.Cast().Any()) { var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray(); var dbValues = LoadDbValues(entityType, uniqueEntities, ResourceHook.BeforeUpdateRelationship, relationships); - var resourcesByRelationship = CreateRelationshipHelper(entityType, node.RelationshipsFromPreviousLayer.GetDependentEntities(), dbValues); + + /// these are the entities of the current node grouped by + /// RelationshipAttributes that occured in the previous layer + /// so it looks like { HasOneAttribute:owner => owner_new }. + /// Note that in the BeforeUpdateRelationship hook of Person, + /// we want want inverse relationship attribute: + /// we now have the one pointing from article -> person, ] + /// but we require the the one that points from person -> article + currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); + currentEntitiesGroupedInverse = ReplaceKeysWithInverseRelationships(currenEntitiesGrouped); + + var resourcesByRelationship = CreateRelationshipHelper(entityType, currentEntitiesGroupedInverse, dbValues); var allowedIds = CallHook(nestedHookcontainer, ResourceHook.BeforeUpdateRelationship, new object[] { GetIds(uniqueEntities), resourcesByRelationship, pipeline }).Cast(); var updated = GetAllowedEntities(uniqueEntities, allowedIds); node.UpdateUnique(updated); @@ -263,23 +281,60 @@ void FireNestedBeforeUpdateHooks(ResourcePipeline pipeline, EntityChildLayer lay } } - // fire the BeforeImplicitUpdateRelationship hook for o1 - var implicitPrincipalTargets = node.RelationshipsFromPreviousLayer.GetPrincipalEntities(); - if (pipeline != ResourcePipeline.Post && implicitPrincipalTargets.Any()) + /// Fire the BeforeImplicitUpdateRelationship hook for owner_old. + /// Note: if the pipeline is Post it means we just created article1, + /// which means we are sure that it isn't related to any other entities yet. + if (pipeline != ResourcePipeline.Post) { - FireForAffectedImplicits(entityType, implicitPrincipalTargets, pipeline, uniqueEntities); + /// To fire a hook for owner_old, we need to first get a reference to it. + /// For this, we need to query the database for the HasOneAttribute:owner + /// relationship of article1, which is referred to as the + /// principal side of the HasOneAttribute:owner relationship. + var principalEntities = node.RelationshipsFromPreviousLayer.GetPrincipalEntities(); + if (principalEntities.Any()) + { + /// owner_old is loaded, which is an "implicitly affected entity" + FireForAffectedImplicits(entityType, principalEntities, pipeline, uniqueEntities); + } } - // fire the BeforeImplicitUpdateRelationship hook for a2 - var dependentEntities = node.RelationshipsFromPreviousLayer.GetDependentEntities(); - if (dependentEntities.Any()) + /// Fire the BeforeImplicitUpdateRelationship hook for article2 + /// For this, we need to query the database for the current owner + /// relationship value of owner_new. + currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); + if (currenEntitiesGrouped.Any()) { - (var implicitDependentTargets, var principalEntityType) = GetDependentImplicitsTargets(dependentEntities); - FireForAffectedImplicits(principalEntityType, implicitDependentTargets, pipeline); + /// dependentEntities is grouped by relationships from previous + /// layer, ie { HasOneAttribute:owner => owner_new }. But + /// to load article2 onto owner_new, we need to have the + /// RelationshipAttribute from owner to article, which is the + /// inverse of HasOneAttribute:owner + currentEntitiesGroupedInverse = ReplaceKeysWithInverseRelationships(currenEntitiesGrouped); + /// Note that currently in the JADNC implementation of hooks, + /// the root layer is ALWAYS homogenous, so we safely assume + /// that for every relationship to the previous layer, the + /// principal type is the same. + PrincipalType principalEntityType = currenEntitiesGrouped.First().Key.PrincipalType; + FireForAffectedImplicits(principalEntityType, currentEntitiesGroupedInverse, pipeline); } } } + /// + /// replaces the keys of the dictionary + /// with its inverse relationship attribute. + /// + /// Entities grouped by relationship attribute + Dictionary ReplaceKeysWithInverseRelationships(Dictionary entitiesByRelationship) + { + /// when Article has one Owner (HasOneAttribute:owner) is set, there is no guarantee + /// that the inverse attribute was also set (Owner has one Article: HasOneAttr:article). + /// If it isn't, JADNC currently knows nothing about this relationship pointing back, and it + /// currently cannot fire hooks for entities resolved through inverse relationships. + var inversableRelationshipAttributes = entitiesByRelationship.Where(kvp => kvp.Key.InverseNavigation != null); + return inversableRelationshipAttributes.ToDictionary(kvp => _graph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); + } + /// /// Given a source of entities, gets the implicitly affected entities /// from the database and calls the BeforeImplicitUpdateRelationship hook. @@ -290,7 +345,8 @@ void FireForAffectedImplicits(Type entityTypeToInclude, Dictionary _graph.GetInverseRelationship(kvp.Key), kvp => kvp.Value); + var resourcesByRelationship = CreateRelationshipHelper(entityTypeToInclude, inverse); CallHook(container, ResourceHook.BeforeImplicitUpdateRelationship, new object[] { resourcesByRelationship, pipeline, }); } @@ -309,18 +365,6 @@ void ValidateHookResponse(IEnumerable returnedList, ResourcePipeline pipel } } - /// - /// NOTE: in JADNC usage, the root layer is ALWAYS homogenous, so we can be sure that for every - /// relationship to the previous layer, the principal type is the same. - /// - (Dictionary, PrincipalType) GetDependentImplicitsTargets(Dictionary dependentEntities) - { - PrincipalType principalType = dependentEntities.First().Key.PrincipalType; - var byInverseRelationship = dependentEntities.Where(kvp => kvp.Key.InverseNavigation != null).ToDictionary(kvp => GetInverseRelationship(kvp.Key), kvp => kvp.Value); - return (byInverseRelationship, principalType); - - } - /// /// A helper method to call a hook on reflectively. /// @@ -367,8 +411,8 @@ Dictionary ReplaceWithDbValues(Dictionary().Select(entity => dbValues.Single(dbEntity => dbEntity.StringId == entity.StringId)).Cast(key.DependentType); - prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.DependentType, replaced); + var replaced = prevLayerRelationships[key].Cast().Select(entity => dbValues.Single(dbEntity => dbEntity.StringId == entity.StringId)).Cast(key.PrincipalType); + prevLayerRelationships[key] = TypeHelper.CreateHashSetFor(key.PrincipalType, replaced); } return prevLayerRelationships; } @@ -383,25 +427,43 @@ HashSet GetAllowedEntities(IEnumerable source, IEnumerable - /// Gets the inverse for + /// given the set of , it will load all the + /// values from the database of these entites. /// - RelationshipAttribute GetInverseRelationship(RelationshipAttribute attribute) - { - return _graph.GetInverseRelationship(attribute); - } - - IEnumerable LoadDbValues(Type containerEntityType, IEnumerable uniqueEntities, ResourceHook targetHook, RelationshipAttribute[] relationshipsToNextLayer) + /// The db values. + /// type of the entities to be loaded + /// The set of entities to load the db values for + /// The hook in which the db values will be displayed. + /// Relationships from to the next layer: + /// this indicates which relationships will be included on . + IEnumerable LoadDbValues(Type entityType, IEnumerable uniqueEntities, ResourceHook targetHook, RelationshipAttribute[] relationshipsToNextLayer) { - if (!_executorHelper.ShouldLoadDbValues(containerEntityType, targetHook)) return null; - return _executorHelper.LoadDbValues(containerEntityType, uniqueEntities, targetHook, relationshipsToNextLayer); + /// We only need to load database values if the target hook of this hook execution + /// cycle is compatible with displaying database values and has this option enabled. + if (!_executorHelper.ShouldLoadDbValues(entityType, targetHook)) return null; + return _executorHelper.LoadDbValues(entityType, uniqueEntities, targetHook, relationshipsToNextLayer); } + /// + /// Fires the AfterUpdateRelationship hook + /// void FireAfterUpdateRelationship(IResourceHookContainer container, IEntityNode node, ResourcePipeline pipeline) { - var resourcesByRelationship = CreateRelationshipHelper(node.EntityType, node.RelationshipsFromPreviousLayer.GetDependentEntities()); + + Dictionary currenEntitiesGrouped = node.RelationshipsFromPreviousLayer.GetDependentEntities(); + /// the relationships attributes in currenEntitiesGrouped will be pointing from a + /// resource in the previouslayer to a resource in the current (nested) layer. + /// For the nested hook we need to replace these attributes with their inverse. + /// See the FireNestedBeforeUpdateHooks method for a more detailed example. + var resourcesByRelationship = CreateRelationshipHelper(node.EntityType, ReplaceKeysWithInverseRelationships(currenEntitiesGrouped)); CallHook(container, ResourceHook.AfterUpdateRelationship, new object[] { resourcesByRelationship, pipeline }); } + /// + /// Returns a list of StringIds from a list of IIdentifiables (). + /// + /// The ids. + /// iidentifiable entities. HashSet GetIds(IEnumerable entities) { return new HashSet(entities.Cast().Select(e => e.StringId)); diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs index a7965f1ec9..7783d041e1 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/RootNode.cs @@ -13,13 +13,15 @@ namespace JsonApiDotNetCore.Hooks /// internal class RootNode : IEntityNode where TEntity : class, IIdentifiable { + private readonly RelationshipProxy[] _allRelationshipsToNextLayer; private HashSet _uniqueEntities; public Type EntityType { get; internal set; } public IEnumerable UniqueEntities { get { return _uniqueEntities; } } - public RelationshipProxy[] RelationshipsToNextLayer { get; private set; } - public Dictionary> PrincipalsToNextLayerByType() + public RelationshipProxy[] RelationshipsToNextLayer { get; } + + public Dictionary> PrincipalsToNextLayerByRelationships() { - return RelationshipsToNextLayer + return _allRelationshipsToNextLayer .GroupBy(proxy => proxy.DependentType) .ToDictionary(gdc => gdc.Key, gdc => gdc.ToDictionary(p => p.Attribute, p => UniqueEntities)); } @@ -37,11 +39,12 @@ public Dictionary PrincipalsToNextLayer() /// public IRelationshipsFromPreviousLayer RelationshipsFromPreviousLayer { get { return null; } } - public RootNode(IEnumerable uniqueEntities, RelationshipProxy[] relationships) + public RootNode(IEnumerable uniqueEntities, RelationshipProxy[] poplatedRelationships, RelationshipProxy[] allRelationships) { EntityType = typeof(TEntity); _uniqueEntities = new HashSet(uniqueEntities); - RelationshipsToNextLayer = relationships; + RelationshipsToNextLayer = poplatedRelationships; + _allRelationshipsToNextLayer = allRelationships; } /// diff --git a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs index 50b155daff..5061cce9bf 100644 --- a/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs +++ b/src/JsonApiDotNetCore/Hooks/Traversal/TraversalHelper.cs @@ -57,8 +57,9 @@ public RootNode CreateRootNode(IEnumerable rootEntiti _processedEntities = new Dictionary>(); RegisterRelationshipProxies(typeof(TEntity)); var uniqueEntities = ProcessEntities(rootEntities); - var relationshipsToNextLayer = GetRelationships(typeof(TEntity)); - return new RootNode(uniqueEntities, relationshipsToNextLayer); + var populatedRelationshipsToNextLayer = GetPopulatedRelationships(typeof(TEntity), uniqueEntities.Cast()); + var allRelationshipsFromType = RelationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.PrincipalType == typeof(TEntity)).ToArray(); + return new RootNode(uniqueEntities, populatedRelationshipsToNextLayer, allRelationshipsFromType); } /// @@ -80,30 +81,38 @@ public EntityChildLayer CreateNextLayer(IEnumerable nodes) { /// first extract entities by parsing populated relationships in the entities /// of previous layer - (var dependents, var principals) = ExtractEntities(nodes); + (var principals, var dependents) = ExtractEntities(nodes); - /// group them conveniently so we can make ChildNodes of them - var dependentsGrouped = GroupByDependentTypeOfRelationship(dependents); + /// group them conveniently so we can make ChildNodes of them: + /// there might be several relationship attributes in dependents dictionary + /// that point to the same dependent type. + var principalsGrouped = GroupByDependentTypeOfRelationship(principals); /// convert the groups into child nodes - var nextNodes = dependentsGrouped.Select(entry => + var nextNodes = principalsGrouped.Select(entry => { var nextNodeType = entry.Key; + RegisterRelationshipProxies(nextNodeType); + + var populatedRelationships = new List(); var relationshipsToPreviousLayer = entry.Value.Select(grouped => { var proxy = grouped.Key; - return CreateRelationsipGroupInstance(nextNodeType, proxy, grouped.Value, principals[proxy]); + populatedRelationships.AddRange(GetPopulatedRelationships(nextNodeType, dependents[proxy])); + return CreateRelationshipGroupInstance(nextNodeType, proxy, grouped.Value, dependents[proxy]); }).ToList(); - RegisterRelationshipProxies(nextNodeType); - return CreateNodeInstance(nextNodeType, GetRelationships(nextNodeType), relationshipsToPreviousLayer); + return CreateNodeInstance(nextNodeType, populatedRelationships.ToArray(), relationshipsToPreviousLayer); }).ToList(); /// wrap the child nodes in a EntityChildLayer return new EntityChildLayer(nextNodes); } - + /// + /// iterates throug the dictinary and groups the values + /// by matching dependent type of the keys (which are relationshipattributes) + /// Dictionary>>> GroupByDependentTypeOfRelationship(Dictionary> relationships) { return relationships.GroupBy(kvp => kvp.Key.DependentType).ToDictionary(gdc => gdc.Key, gdc => gdc.ToList()); @@ -115,11 +124,8 @@ Dictionary (Dictionary>, Dictionary>) ExtractEntities(IEnumerable principalNodes) { - var currentLayerEntities = new List(); - var principalsGrouped = new Dictionary>(); - var dependentsGrouped = new Dictionary>(); - - principalNodes.ForEach(n => RegisterRelationshipProxies(n.EntityType)); + var principalsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => prevLayerEntities + var dependentsEntitiesGrouped = new Dictionary>(); // RelationshipAttr_prevlayer->currentlayer => currentLayerEntities foreach (var node in principalNodes) { @@ -141,36 +147,36 @@ Dictionary(), proxy.DependentType); - if (proxy.IsContextRelation || newDependentEntities.Any()) + var uniqueDependentEntities = UniqueInTree(dependentEntities.Cast(), proxy.DependentType); + if (proxy.IsContextRelation || uniqueDependentEntities.Any()) { - currentLayerEntities.AddRange(newDependentEntities); - AddToRelationshipGroup(dependentsGrouped, proxy, newDependentEntities); // TODO check if this needs to be newDependentEntities or just dependentEntities - AddToRelationshipGroup(principalsGrouped, proxy, new IIdentifiable[] { principalEntity }); + AddToRelationshipGroup(dependentsEntitiesGrouped, proxy, uniqueDependentEntities); + AddToRelationshipGroup(principalsEntitiesGrouped, proxy, new IIdentifiable[] { principalEntity }); } } } } - var processEntities = GetType().GetMethod(nameof(ProcessEntities), BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var kvp in dependentsGrouped) + var processEntitiesMethod = GetType().GetMethod(nameof(ProcessEntities), BindingFlags.NonPublic | BindingFlags.Instance); + foreach (var kvp in dependentsEntitiesGrouped) { var type = kvp.Key.DependentType; var list = kvp.Value.Cast(type); - processEntities.MakeGenericMethod(type).Invoke(this, new object[] { list }); + processEntitiesMethod.MakeGenericMethod(type).Invoke(this, new object[] { list }); } - return (principalsGrouped, dependentsGrouped); + return (principalsEntitiesGrouped, dependentsEntitiesGrouped); } /// - /// Get all relationships known in the current tree traversal from a + /// Get all populated relationships known in the current tree traversal from a /// principal type to any dependent type /// /// The relationships. - RelationshipProxy[] GetRelationships(PrincipalType principal) + RelationshipProxy[] GetPopulatedRelationships(PrincipalType principalType, IEnumerable principals) { - return RelationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.PrincipalType == principal).ToArray(); + var relationshipsFromPrincipalToDependent = RelationshipProxies.Select(entry => entry.Value).Where(proxy => proxy.PrincipalType == principalType); + return relationshipsFromPrincipalToDependent.Where(proxy => proxy.IsContextRelation || principals.Any(p => proxy.GetValue(p) != null)).ToArray(); } /// @@ -187,6 +193,11 @@ HashSet ProcessEntities(IEnumerable incomingEntities) return newEntities; } + /// + /// Parses all relationships from to + /// other models in the resource graphs by constructing RelationshipProxies . + /// + /// The type to parse void RegisterRelationshipProxies(DependentType type) { var contextEntity = _graph.GetContextEntity(type); @@ -196,8 +207,9 @@ void RegisterRelationshipProxies(DependentType type) if (!RelationshipProxies.TryGetValue(attr, out RelationshipProxy proxies)) { DependentType dependentType = GetDependentTypeFromRelationship(attr); - var isContextRelation = _context.RelationshipsToUpdate?.ContainsKey(attr); - var proxy = new RelationshipProxy(attr, dependentType, isContextRelation != null && (bool)isContextRelation); + bool isContextRelation = false; + if (_context.RelationshipsToUpdate != null) isContextRelation = _context.RelationshipsToUpdate.ContainsKey(attr); + var proxy = new RelationshipProxy(attr, dependentType, isContextRelation); RelationshipProxies[attr] = proxy; } } @@ -291,7 +303,7 @@ IRelationshipsFromPreviousLayer CreateRelationshipsFromInstance(DependentType no /// /// Reflective helper method to create an instance of ; /// - IRelationshipGroup CreateRelationsipGroupInstance(Type thisLayerType, RelationshipProxy proxy, List principalEntities, List dependentEntities) + IRelationshipGroup CreateRelationshipGroupInstance(Type thisLayerType, RelationshipProxy proxy, List principalEntities, List dependentEntities) { var dependentEntitiesHashed = TypeHelper.CreateInstanceOfOpenType(typeof(HashSet<>), thisLayerType, dependentEntities.Cast(thisLayerType)); return (IRelationshipGroup)TypeHelper.CreateInstanceOfOpenType(typeof(RelationshipGroup<>), diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index cf642aa395..7326377b3d 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Hooks; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Internal @@ -113,9 +114,9 @@ public static object CreateInstanceOfOpenType(Type openType, Type[] parameters, /// /// Helper method that "unboxes" the TValue from the relationship dictionary into /// - public static Dictionary> ConvertRelationshipDictionary(Dictionary relationships) + public static Dictionary> ConvertRelationshipDictionary(Dictionary relationships) { - return relationships.ToDictionary(pair => pair.Key, pair => (HashSet)pair.Value); + return relationships.ToDictionary(pair => pair.Key, pair => (HashSet)pair.Value); } /// @@ -180,5 +181,23 @@ public static Type GetListInnerType(IEnumerable list) { return list.GetType().GetGenericArguments()[0]; } + + /// + /// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable + /// + public static Type GetIdentifierType(Type entityType) + { + var property = entityType.GetProperty("Id"); + if (property == null) throw new ArgumentException("Type does not have a property Id"); + return entityType.GetProperty("Id").PropertyType; + } + + /// + /// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable + /// + public static Type GetIdentifierType() where T : IIdentifiable + { + return typeof(T).GetProperty("Id").PropertyType; + } } } diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index e615de6538..0a6b0ac091 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -173,19 +173,19 @@ public virtual void AfterUpdate(HashSet entities, ResourcePipeline pipeline) /// public virtual void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } /// - public virtual void AfterUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } + public virtual void AfterUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { } /// - public virtual IEnumerable BeforeCreate(IEntityHashSet affected, ResourcePipeline pipeline) { return affected; } + public virtual IEnumerable BeforeCreate(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } /// public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { } /// - public virtual IEnumerable BeforeUpdate(IEntityDiff ResourceDiff, ResourcePipeline pipeline) { return ResourceDiff.Entities; } + public virtual IEnumerable BeforeUpdate(IEntityDiffs entityDiff, ResourcePipeline pipeline) { return entityDiff.Entities; } /// - public virtual IEnumerable BeforeDelete(IEntityHashSet affected, ResourcePipeline pipeline) { return affected; } + public virtual IEnumerable BeforeDelete(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } /// - public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { return ids; } + public virtual IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { return ids; } /// - public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline) { } + public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline) { } /// public virtual IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) { return entities; } diff --git a/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs b/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs new file mode 100644 index 0000000000..39513d05b3 --- /dev/null +++ b/test/UnitTests/ResourceHooks/AffectedEntitiesHelperTests.cs @@ -0,0 +1,162 @@ +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Hooks; +using System.Collections.Generic; +using Xunit; +using System.Linq; + +namespace UnitTests.ResourceHooks.AffectedEntities +{ + public class Dummy : Identifiable { } + public class NotTargeted : Identifiable { } + public class ToMany : Identifiable { } + public class ToOne : Identifiable { } + + public class AffectedEntitiesHelperTests + { + public readonly HasOneAttribute FirstToOneAttr; + public readonly HasOneAttribute SecondToOneAttr; + public readonly HasManyAttribute ToManyAttr; + + public readonly Dictionary> Relationships = new Dictionary>(); + public readonly HashSet FirstToOnesEntities = new HashSet { new Dummy() { Id = 1 }, new Dummy() { Id = 2 }, new Dummy() { Id = 3 } }; + public readonly HashSet SecondToOnesEntities = new HashSet { new Dummy() { Id = 4 }, new Dummy() { Id = 5 }, new Dummy() { Id = 6 } }; + public readonly HashSet ToManiesEntities = new HashSet { new Dummy() { Id = 7 }, new Dummy() { Id = 8 }, new Dummy() { Id = 9 } }; + public readonly HashSet NoRelationshipsEntities = new HashSet { new Dummy() { Id = 10 }, new Dummy() { Id = 11 }, new Dummy() { Id = 12 } }; + public readonly HashSet AllEntities; + public AffectedEntitiesHelperTests() + { + FirstToOneAttr = new HasOneAttribute("first-to-one") + { + PrincipalType = typeof(Dummy), + DependentType = typeof(ToOne) + }; + SecondToOneAttr = new HasOneAttribute("second-to-one") + { + PrincipalType = typeof(Dummy), + DependentType = typeof(ToOne) + }; + ToManyAttr = new HasManyAttribute("to-manies") + { + PrincipalType = typeof(Dummy), + DependentType = typeof(ToMany) + }; + Relationships.Add(FirstToOneAttr, FirstToOnesEntities); + Relationships.Add(SecondToOneAttr, SecondToOnesEntities); + Relationships.Add(ToManyAttr, ToManiesEntities); + AllEntities = new HashSet(FirstToOnesEntities.Union(SecondToOnesEntities).Union(ToManiesEntities).Union(NoRelationshipsEntities)); + } + + [Fact] + public void RelationshipsDictionary_GetByRelationships() + { + // arrange + RelationshipsDictionary relationshipsDictionary = new RelationshipsDictionary(Relationships); + + // act + Dictionary> toOnes = relationshipsDictionary.GetByRelationship(); + Dictionary> toManies = relationshipsDictionary.GetByRelationship(); + Dictionary> notTargeted = relationshipsDictionary.GetByRelationship(); + + // assert + AssertRelationshipDictionaryGetters(relationshipsDictionary, toOnes, toManies, notTargeted); + } + + [Fact] + public void EntityHashSet_GetByRelationships() + { + // arrange + EntityHashSet entities = new EntityHashSet(AllEntities, Relationships); + + // act + Dictionary> toOnes = entities.GetByRelationship(); + Dictionary> toManies = entities.GetByRelationship(); + Dictionary> notTargeted = entities.GetByRelationship(); + Dictionary> allRelationships = entities.AffectedRelationships; + + // Assert + AssertRelationshipDictionaryGetters(allRelationships, toOnes, toManies, notTargeted); + var allEntitiesWithAffectedRelationships = allRelationships.SelectMany(kvp => kvp.Value).ToList(); + NoRelationshipsEntities.ToList().ForEach(e => + { + Assert.DoesNotContain(e, allEntitiesWithAffectedRelationships); + }); + } + + [Fact] + public void EntityDiff_GetByRelationships() + { + // arrange + var dbEntities = new HashSet(AllEntities.Select(e => new Dummy { Id = e.Id }).ToList()); + EntityDiffs diffs = new EntityDiffs(AllEntities, dbEntities, Relationships); + + // act + Dictionary> toOnes = diffs.Entities.GetByRelationship(); + Dictionary> toManies = diffs.Entities.GetByRelationship(); + Dictionary> notTargeted = diffs.Entities.GetByRelationship(); + Dictionary> allRelationships = diffs.Entities.AffectedRelationships; + + // Assert + AssertRelationshipDictionaryGetters(allRelationships, toOnes, toManies, notTargeted); + var allEntitiesWithAffectedRelationships = allRelationships.SelectMany(kvp => kvp.Value).ToList(); + NoRelationshipsEntities.ToList().ForEach(e => + { + Assert.DoesNotContain(e, allEntitiesWithAffectedRelationships); + }); + + var requestEntitiesFromDiff = diffs.Entities; + requestEntitiesFromDiff.ToList().ForEach(e => + { + Assert.Contains(e, AllEntities); + }); + var databaseEntitiesFromDiff = diffs.DatabaseValues; + databaseEntitiesFromDiff.ToList().ForEach(e => + { + Assert.Contains(e, dbEntities); + }); + } + + [Fact] + public void EntityDiff_Loops_Over_Diffs() + { + // arrange + var dbEntities = new HashSet(AllEntities.Select(e => new Dummy { Id = e.Id })); + EntityDiffs diffs = new EntityDiffs(AllEntities, dbEntities, Relationships); + + // Assert & act + foreach (EntityDiffPair diff in diffs) + { + Assert.Equal(diff.Entity.Id, diff.DatabaseValue.Id); + Assert.NotEqual(diff.Entity, diff.DatabaseValue); + Assert.Contains(diff.Entity, AllEntities); + Assert.Contains(diff.DatabaseValue, dbEntities); + } + } + + private void AssertRelationshipDictionaryGetters(Dictionary> relationshipsDictionary, + Dictionary> toOnes, + Dictionary> toManies, + Dictionary> notTargeted) + { + Assert.Contains(FirstToOneAttr, toOnes.Keys); + Assert.Contains(SecondToOneAttr, toOnes.Keys); + Assert.Contains(ToManyAttr, toManies.Keys); + Assert.Equal(relationshipsDictionary.Keys.Count, toOnes.Keys.Count + toManies.Keys.Count + notTargeted.Keys.Count); + + toOnes[FirstToOneAttr].ToList().ForEach((entitiy) => + { + Assert.Contains(entitiy, FirstToOnesEntities); + }); + + toOnes[SecondToOneAttr].ToList().ForEach((entity) => + { + Assert.Contains(entity, SecondToOnesEntities); + }); + + toManies[ToManyAttr].ToList().ForEach((entitiy) => + { + Assert.Contains(entitiy, ToManiesEntities); + }); + Assert.Empty(notTargeted); + } + } +} diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs index e27f7f8caa..f02acda264 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdateTests.cs @@ -24,7 +24,7 @@ public void BeforeUpdate() hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship(It.IsAny>(), It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } @@ -62,7 +62,7 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.IsAny>(), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } diff --git a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs index dd1eeea5e1..fca4d9c480 100644 --- a/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs +++ b/test/UnitTests/ResourceHooks/ResourceHookExecutor/Update/BeforeUpdate_WithDbValues_Tests.cs @@ -59,7 +59,7 @@ public void BeforeUpdate() hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheck(diff, description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), It.Is>(rh => PersonCheck(lastName, rh)), @@ -93,7 +93,7 @@ public void BeforeUpdate_Deleting_Relationship() hookExecutor.BeforeUpdate(_todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheck(diff, description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( It.Is>(rh => PersonCheck(lastName + lastName, rh)), ResourcePipeline.Patch), @@ -140,7 +140,7 @@ public void BeforeUpdate_Without_Child_Hook_Implemented() hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheck(diff, description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); todoResourceMock.Verify(rd => rd.BeforeImplicitUpdateRelationship( It.Is>(rh => TodoCheck(rh, description + description)), ResourcePipeline.Patch), @@ -161,7 +161,7 @@ public void BeforeUpdate_NoImplicit() hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheck(diff, description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); ownerResourceMock.Verify(rd => rd.BeforeUpdateRelationship( It.Is>(ids => PersonIdCheck(ids, personId)), It.IsAny>(), @@ -204,16 +204,21 @@ public void BeforeUpdate_NoImplicit_Without_Child_Hook_Implemented() hookExecutor.BeforeUpdate(todoList, ResourcePipeline.Patch); // assert - todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheck(diff, description)), ResourcePipeline.Patch), Times.Once()); + todoResourceMock.Verify(rd => rd.BeforeUpdate(It.Is>((diff) => TodoCheckDiff(diff, description)), ResourcePipeline.Patch), Times.Once()); VerifyNoOtherCalls(todoResourceMock, ownerResourceMock); } - private bool TodoCheck(IEntityDiff diff, string checksum) + private bool TodoCheckDiff(IEntityDiffs diff, string checksum) { var diffPair = diff.Single(); var dbCheck = diffPair.DatabaseValue.Description == checksum; var reqCheck = diffPair.Entity.Description == null; - return (dbCheck && reqCheck); + var diffPairCheck = (dbCheck && reqCheck); + + var updatedRelationship = diff.Entities.GetByRelationship().Single(); + var diffcheck = updatedRelationship.Key.PublicRelationshipName == "one-to-one-person"; + + return (dbCheck && reqCheck && diffcheck); } private bool TodoCheck(IRelationshipsDictionary rh, string checksum) diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index 369e46b26d..17065d3bec 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -39,6 +39,7 @@ public HooksDummyData() .AddResource
() .AddResource() .AddResource() + .AddResource() .Build(); _todoFaker = new Faker().Rules((f, i) => i.Id = f.UniqueIndex + 1); @@ -129,7 +130,6 @@ protected List CreateTodoWithOwner() var articles = new List
() { articleTagsSubset, articleWithAllTags }; return (articles, allJoins, allTags); } - } public class HooksTestsSetup : HooksDummyData @@ -168,7 +168,6 @@ public class HooksTestsSetup : HooksDummyData // mocking the GenericProcessorFactory and JsonApiContext and wiring them up. (var context, var processorFactory) = CreateContextAndProcessorMocks(); - var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; SetupProcessorFactoryForResourceDefinition(processorFactory, mainResource.Object, mainDiscovery, context.Object, dbContext); @@ -261,7 +260,7 @@ void MockHooks(Mock> resourceDefinition) .Setup(rd => rd.BeforeRead(It.IsAny(), It.IsAny(), It.IsAny())) .Verifiable(); resourceDefinition - .Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) + .Setup(rd => rd.BeforeUpdate(It.IsAny>(), It.IsAny())) .Returns, ResourcePipeline>((entityDiff, context) => entityDiff.Entities) .Verifiable(); resourceDefinition @@ -275,12 +274,10 @@ void MockHooks(Mock> resourceDefinition) resourceDefinition .Setup(rd => rd.BeforeImplicitUpdateRelationship(It.IsAny>(), It.IsAny())) .Verifiable(); - resourceDefinition .Setup(rd => rd.OnReturn(It.IsAny>(), It.IsAny())) .Returns, ResourcePipeline>((entities, context) => entities) .Verifiable(); - resourceDefinition .Setup(rd => rd.AfterCreate(It.IsAny>(), It.IsAny())) .Verifiable(); @@ -293,8 +290,6 @@ void MockHooks(Mock> resourceDefinition) resourceDefinition .Setup(rd => rd.AfterDelete(It.IsAny>(), It.IsAny(), It.IsAny())) .Verifiable(); - - } (Mock, Mock) CreateContextAndProcessorMocks() @@ -325,8 +320,16 @@ void SetupProcessorFactoryForResourceDefinition( if (dbContext != null) { - IEntityReadRepository repo = CreateTestRepository(dbContext, apiContext); - processorFactory.Setup(c => c.GetProcessor>(typeof(IEntityReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); + var idType = TypeHelper.GetIdentifierType(); + if (idType == typeof(int)) + { + IEntityReadRepository repo = CreateTestRepository(dbContext, apiContext); + processorFactory.Setup(c => c.GetProcessor>(typeof(IEntityReadRepository<,>), typeof(TModel), typeof(int))).Returns(repo); + } else + { + throw new TypeLoadException("Test not set up properly"); + } + } }