Skip to content

Commit 59a9359

Browse files
committed
feat: IAffectedResource, IEntityDiff, IAffectedRelationships have upgraded for better quality of life
1 parent 8fc015a commit 59a9359

File tree

18 files changed

+168
-51
lines changed

18 files changed

+168
-51
lines changed

src/Examples/JsonApiDotNetCoreExample/Resources/PassportResource.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ public PassportResource(IResourceGraph graph) : base(graph)
1414
{
1515
}
1616

17-
public override void BeforeRead(ResourcePipeline pipeline, bool nestedHook = false, string stringId = null)
17+
public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null)
1818
{
19-
if (pipeline == ResourcePipeline.GetSingle && nestedHook)
19+
if (pipeline == ResourcePipeline.GetSingle && isIncluded)
2020
{
2121
throw new JsonApiException(403, "Not allowed to include passports on individual people", new UnauthorizedAccessException());
2222
}

src/Examples/JsonApiDotNetCoreExample/Resources/PersonResource.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public override IEnumerable<string> BeforeUpdateRelationship(HashSet<string> ids
1616
return ids;
1717
}
1818

19+
//[LoadDatabaseValues(true)]
20+
//public override IEnumerable<Person> BeforeUpdate(IEntityDiff<Person> entityDiff, ResourcePipeline pipeline)
21+
//{
22+
// return entityDiff.Entities;
23+
//}
24+
1925
public override void BeforeImplicitUpdateRelationship(IAffectedRelationships<Person> resourcesByRelationship, ResourcePipeline pipeline)
2026
{
2127
resourcesByRelationship.GetByRelationship<Passport>().ToList().ForEach(kvp => DisallowLocked(kvp.Value));

src/JsonApiDotNetCore/Hooks/Execution/AffectedRelationships.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public Dictionary<RelationshipAttribute, HashSet<TDependent>> AllByRelationships
3636
{
3737
return _groups?.ToDictionary(p => p.Key.Attribute, p => p.Value);
3838
}
39-
public AffectedRelationships(Dictionary<RelationshipProxy, IEnumerable> relationships)
39+
internal AffectedRelationships(Dictionary<RelationshipProxy, IEnumerable> relationships)
4040
{
4141
_groups = relationships.ToDictionary(kvp => kvp.Key, kvp => new HashSet<TDependent>((IEnumerable<TDependent>)kvp.Value));
4242
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.Collections.Generic;
2+
using JsonApiDotNetCore.Models;
3+
using System.Linq;
4+
using System.Collections;
5+
6+
namespace JsonApiDotNetCore.Hooks
7+
{
8+
9+
public interface IAffectedResources<TEntity> : IAffectedResourcesBase<TEntity>, IEnumerable<TEntity> where TEntity : class, IIdentifiable
10+
{
11+
12+
}
13+
14+
/// NOTE: you might wonder why there is a separate AffectedResourceBase and AffectedResource.
15+
/// If we merge them together, ie get rid of the base and just let the AffectedResource directly implement IEnumerable{TEntity},
16+
/// we will run in to problems with the following:
17+
/// EntityDiff{<typeparam name="TEntity"/>} inherits from AffectedResource{TEntity},
18+
/// but EntityDiff also implements IEnumerable{EntityDiffPair{TEntity}}. This means that
19+
/// EntityDiff will implement two IEnumerable{x} where (x1 = TEntity and x2 = EntityDiffPair{TEntity} )
20+
/// The problem with this is that when you then try to do a simple foreach loop over
21+
/// a EntityDiff instance, it will throw an error, because it does not know which of the two enumerators to pick.
22+
/// We want EntityDiff to only loop over the EntityDiffPair, so we can do that by making sure
23+
/// it doesn't inherit the IEnumerable{TEntity} part from AffectedResources.
24+
public interface IAffectedResourcesBase<TEntity> where TEntity : class, IIdentifiable
25+
{
26+
HashSet<TEntity> Entities { get; }
27+
}
28+
29+
public class AffectedResources<TEntity> : AffectedResourcesBase<TEntity>, IAffectedResources<TEntity> where TEntity : class, IIdentifiable
30+
{
31+
internal AffectedResources(IEnumerable entities,
32+
Dictionary<RelationshipProxy, IEnumerable> relationships)
33+
: base(entities, relationships) { }
34+
35+
public IEnumerator<TEntity> GetEnumerator()
36+
{
37+
return Entities.GetEnumerator();
38+
}
39+
40+
IEnumerator IEnumerable.GetEnumerator()
41+
{
42+
return GetEnumerator();
43+
}
44+
}
45+
46+
public abstract class AffectedResourcesBase<TEntity> : AffectedRelationships<TEntity>, IAffectedResourcesBase<TEntity> where TEntity : class, IIdentifiable
47+
{
48+
public HashSet<TEntity> Entities { get; }
49+
50+
internal protected AffectedResourcesBase(IEnumerable entities,
51+
Dictionary<RelationshipProxy, IEnumerable> relationships) : base(relationships)
52+
{
53+
Entities = new HashSet<TEntity>(entities.Cast<TEntity>());
54+
}
55+
}
56+
57+
}
Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-

2-
using System;
1+
using System;
32
using System.Collections;
43
using System.Collections.Generic;
54
using System.Linq;
6-
using JsonApiDotNetCore.Internal;
75
using JsonApiDotNetCore.Models;
86

97
namespace JsonApiDotNetCore.Hooks
108
{
11-
129
/// <summary>
1310
/// A helper class that provides insight in what is to be updated. The
1411
/// <see cref="IEntityDiff{TEntity}.RequestEntities"/> property reflects what was parsed from the incoming request,
@@ -17,29 +14,55 @@ namespace JsonApiDotNetCore.Hooks
1714
/// Any relationships that are updated can be retrieved via the methods implemented on
1815
/// <see cref="IAffectedRelationships{TDependent}"/>.
1916
/// </summary>
20-
public interface IEntityDiff<TEntity> : IAffectedRelationships<TEntity> where TEntity : class, IIdentifiable
17+
public interface IEntityDiff<TEntity> : IEnumerable<EntityDiffPair<TEntity>>, IAffectedResourcesBase<TEntity> where TEntity : class, IIdentifiable
2118
{
22-
HashSet<TEntity> RequestEntities { get; }
2319
HashSet<TEntity> DatabaseEntities { get; }
2420
}
2521

26-
public class EntityDiff<TEntity> : AffectedRelationships<TEntity>, IEntityDiff<TEntity> where TEntity : class, IIdentifiable
22+
public class EntityDiff<TEntity> : AffectedResourcesBase<TEntity>, IEntityDiff<TEntity> where TEntity : class, IIdentifiable
2723
{
2824
private readonly HashSet<TEntity> _databaseEntities;
25+
private readonly bool _databaseValuesLoaded;
2926
public HashSet<TEntity> DatabaseEntities { get => _databaseEntities ?? ThrowNoDbValuesError(); }
3027

31-
public HashSet<TEntity> RequestEntities { get; private set; }
32-
public EntityDiff(IEnumerable requestEntities,
28+
internal EntityDiff(IEnumerable requestEntities,
3329
IEnumerable databaseEntities,
34-
Dictionary<RelationshipProxy, IEnumerable> relationships) : base(relationships)
30+
Dictionary<RelationshipProxy, IEnumerable> relationships) : base(requestEntities, relationships)
3531
{
36-
RequestEntities = (HashSet<TEntity>)requestEntities;
3732
_databaseEntities = (HashSet<TEntity>)databaseEntities;
33+
_databaseValuesLoaded |= _databaseEntities != null;
3834
}
3935

4036
private HashSet<TEntity> ThrowNoDbValuesError()
4137
{
4238
throw new MemberAccessException("Cannot access database entities if the LoadDatabaseValues option is set to false");
4339
}
40+
41+
public IEnumerator<EntityDiffPair<TEntity>> GetEnumerator()
42+
{
43+
foreach (var entity in Entities)
44+
{
45+
TEntity currentValueInDatabase = null;
46+
if (_databaseValuesLoaded) currentValueInDatabase = _databaseEntities.Single(e => entity.StringId == e.StringId);
47+
yield return new EntityDiffPair<TEntity>(entity, currentValueInDatabase);
48+
}
49+
}
50+
51+
IEnumerator IEnumerable.GetEnumerator()
52+
{
53+
return GetEnumerator();
54+
}
55+
}
56+
57+
public class EntityDiffPair<TEntity> where TEntity : class, IIdentifiable
58+
{
59+
internal EntityDiffPair(TEntity entity, TEntity databaseValue)
60+
{
61+
Entity = entity;
62+
DatabaseValue = databaseValue;
63+
}
64+
65+
public TEntity Entity { get; private set; }
66+
public TEntity DatabaseValue { get; private set; }
4467
}
4568
}

src/JsonApiDotNetCore/Hooks/IResourceHookContainer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public interface IBeforeHooks<TEntity> where TEntity : class, IIdentifiable
9696
/// <returns>The transformed entity set</returns>
9797
/// <param name="entities">The unique set of affected entities.</param>
9898
/// <param name="pipeline">An enum indicating from where the hook was triggered.</param>
99-
IEnumerable<TEntity> BeforeCreate(HashSet<TEntity> entities, ResourcePipeline pipeline);
99+
IEnumerable<TEntity> BeforeCreate(IAffectedResources<TEntity> entities, ResourcePipeline pipeline);
100100
/// <summary>
101101
/// Implement this hook to run custom logic in the <see cref=" EntityResourceService{T}"/>
102102
/// layer just before reading entities of type <typeparamref name="TEntity"/>.
@@ -133,7 +133,7 @@ public interface IBeforeHooks<TEntity> where TEntity : class, IIdentifiable
133133
/// <returns>The transformed entity set</returns>
134134
/// <param name="entityDiff">The entity diff.</param>
135135
/// <param name="pipeline">An enum indicating from where the hook was triggered.</param>
136-
IEnumerable<TEntity> BeforeUpdate(EntityDiff<TEntity> entityDiff, ResourcePipeline pipeline);
136+
IEnumerable<TEntity> BeforeUpdate(IEntityDiff<TEntity> entityDiff, ResourcePipeline pipeline);
137137
/// <summary>
138138
/// Implement this hook to run custom logic in the <see cref=" EntityResourceService{T}"/>
139139
/// layer just before deleting entities of type <typeparamref name="TEntity"/>.
@@ -155,7 +155,7 @@ public interface IBeforeHooks<TEntity> where TEntity : class, IIdentifiable
155155
/// <returns>The transformed entity set</returns>
156156
/// <param name="entities">The unique set of affected entities.</param>
157157
/// <param name="pipeline">An enum indicating from where the hook was triggered.</param>
158-
IEnumerable<TEntity> BeforeDelete(HashSet<TEntity> entities, ResourcePipeline pipeline);
158+
IEnumerable<TEntity> BeforeDelete(IAffectedResources<TEntity> entities, ResourcePipeline pipeline);
159159
/// <summary>
160160
/// Implement this hook to run custom logic in the <see cref=" EntityResourceService{T}"/>
161161
/// layer just before updating relationships to entities of type <typeparamref name="TEntity"/>.

src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public virtual IEnumerable<TEntity> BeforeCreate<TEntity>(IEnumerable<TEntity> e
6464
{
6565
if (GetHook(ResourceHook.BeforeCreate, entities, out var container, out var node))
6666
{
67-
IEnumerable<TEntity> updated = container.BeforeCreate((HashSet<TEntity>)node.UniqueEntities, pipeline);
67+
var affected = new AffectedResources<TEntity>((HashSet<TEntity>)node.UniqueEntities, node.PrincipalsToNextLayer());
68+
IEnumerable<TEntity> updated = container.BeforeCreate(affected, pipeline);
6869
node.UpdateUnique(updated);
6970
node.Reassign(entities);
7071
}
@@ -78,8 +79,10 @@ public virtual IEnumerable<TEntity> BeforeDelete<TEntity>(IEnumerable<TEntity> e
7879
{
7980
if (GetHook(ResourceHook.BeforeDelete, entities, out var container, out var node))
8081
{
81-
var targetEntities = (LoadDbValues(typeof(TEntity), (IEnumerable<TEntity>)node.UniqueEntities, ResourceHook.BeforeDelete, node.RelationshipsToNextLayer) ?? node.UniqueEntities);
82-
IEnumerable<TEntity> updated = container.BeforeDelete((HashSet<TEntity>)targetEntities, pipeline);
82+
var targetEntities = LoadDbValues(typeof(TEntity), (IEnumerable<TEntity>)node.UniqueEntities, ResourceHook.BeforeDelete, node.RelationshipsToNextLayer) ?? node.UniqueEntities;
83+
var affected = new AffectedResources<TEntity>(targetEntities, node.PrincipalsToNextLayer());
84+
85+
IEnumerable<TEntity> updated = container.BeforeDelete(affected, pipeline);
8386
node.UpdateUnique(updated);
8487
node.Reassign(entities);
8588
}
@@ -350,7 +353,7 @@ object ThrowJsonApiExceptionOnError(Func<object> action)
350353
IAffectedRelationships CreateRelationshipHelper(DependentType entityType, Dictionary<RelationshipProxy, IEnumerable> prevLayerRelationships, IEnumerable dbValues = null)
351354
{
352355
if (dbValues != null) ReplaceWithDbValues(prevLayerRelationships, dbValues.Cast<IIdentifiable>());
353-
return (IAffectedRelationships)TypeHelper.CreateInstanceOfOpenType(typeof(AffectedRelationships<>), entityType, prevLayerRelationships);
356+
return (IAffectedRelationships)TypeHelper.CreateInstanceOfOpenType(typeof(AffectedRelationships<>), entityType, true, prevLayerRelationships);
354357
}
355358

356359
/// <summary>

src/JsonApiDotNetCore/Internal/TypeHelper.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,18 @@ public static object CreateInstanceOfOpenType(Type openType, Type[] parameters,
104104
return Activator.CreateInstance(parameterizedType, constructorArguments);
105105
}
106106

107+
/// <summary>
108+
/// Use this overload if you need to instantiate a type that has a internal constructor
109+
/// </summary>
110+
public static object CreateInstanceOfOpenType(Type openType, Type[] parameters, bool hasInternalConstructor, params object[] constructorArguments)
111+
{
112+
if (!hasInternalConstructor) return CreateInstanceOfOpenType(openType, parameters, constructorArguments);
113+
var parameterizedType = openType.MakeGenericType(parameters);
114+
// note that if for whatever reason the constructor of AffectedResource is set from
115+
// internal to public, this will throw an error, as it is looking for a no
116+
return Activator.CreateInstance(parameterizedType, BindingFlags.NonPublic | BindingFlags.Instance, null, constructorArguments, null);
117+
}
118+
107119
/// <summary>
108120
/// Creates an instance of the specified generic type
109121
/// </summary>
@@ -116,6 +128,15 @@ public static object CreateInstanceOfOpenType(Type openType, Type parameter, par
116128
return CreateInstanceOfOpenType(openType, new Type[] { parameter }, constructorArguments);
117129
}
118130

131+
/// <summary>
132+
/// Use this overload if you need to instantiate a type that has a internal constructor
133+
/// </summary>
134+
public static object CreateInstanceOfOpenType(Type openType, Type parameter, bool hasInternalConstructor, params object[] constructorArguments)
135+
{
136+
return CreateInstanceOfOpenType(openType, new Type[] { parameter }, hasInternalConstructor, constructorArguments);
137+
138+
}
139+
119140
/// <summary>
120141
/// Reflectively instantiates a list of a certain type.
121142
/// </summary>

src/JsonApiDotNetCore/Models/ResourceDefinition.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,13 @@ public virtual void AfterDelete(HashSet<T> entities, ResourcePipeline pipeline,
175175
/// <inheritdoc/>
176176
public virtual void AfterUpdateRelationship(IAffectedRelationships<T> resourcesByRelationship, ResourcePipeline pipeline) { }
177177
/// <inheritdoc/>
178-
public virtual IEnumerable<T> BeforeCreate(HashSet<T> entities, ResourcePipeline pipeline) { return entities; }
178+
public virtual IEnumerable<T> BeforeCreate(IAffectedResources<T> affected, ResourcePipeline pipeline) { return affected; }
179179
/// <inheritdoc/>
180180
public virtual void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) { }
181181
/// <inheritdoc/>
182-
public virtual IEnumerable<T> BeforeUpdate(EntityDiff<T> entityDiff, ResourcePipeline pipeline) { return entityDiff.RequestEntities; }
182+
public virtual IEnumerable<T> BeforeUpdate(IEntityDiff<T> entityDiff, ResourcePipeline pipeline) { return entityDiff.Entities; }
183183
/// <inheritdoc/>
184-
public virtual IEnumerable<T> BeforeDelete(HashSet<T> entities, ResourcePipeline pipeline) { return entities; }
184+
public virtual IEnumerable<T> BeforeDelete(IAffectedResources<T> affected, ResourcePipeline pipeline) { return affected; }
185185
/// <inheritdoc/>
186186
public virtual IEnumerable<string> BeforeUpdateRelationship(HashSet<string> ids, IAffectedRelationships<T> resourcesByRelationship, ResourcePipeline pipeline) { return ids; }
187187
/// <inheritdoc/>

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ public async Task Can_Patch_Entity()
147147
[Fact]
148148
public async Task Patch_Entity_With_HasMany_Does_Not_Included_Relationships()
149149
{
150+
/// @TODO: if we add a BeforeUpate resource hook to PersonDefinition
151+
/// with database values enabled, this test will fail because todo-items
152+
/// will be included in the person instance in the database-value loading.
153+
/// This is then attached in the EF dbcontext, so when the query is executed and returned,
154+
/// that entity will still have the relationship included even though the repo didn't include it.
155+
156+
150157
// arrange
151158
var todoItem = _todoItemFaker.Generate();
152159
var person = _personFaker.Generate();

test/UnitTests/ResourceHooks/DiscoveryTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class DummyResourceDefinition : ResourceDefinition<Dummy>
1414
{
1515
public DummyResourceDefinition() : base(new ResourceGraphBuilder().AddResource<Dummy>().Build()) { }
1616

17-
public override IEnumerable<Dummy> BeforeDelete(HashSet<Dummy> entities, ResourcePipeline pipeline) { return entities; }
17+
public override IEnumerable<Dummy> BeforeDelete(IAffectedResources<Dummy> affected, ResourcePipeline pipeline) { return affected; }
1818
public override void AfterDelete(HashSet<Dummy> entities, ResourcePipeline pipeline, bool succeeded) { }
1919
}
2020

@@ -35,7 +35,7 @@ public abstract class ResourceDefintionBase<T> : ResourceDefinition<T> where T :
3535
{
3636
protected ResourceDefintionBase(IResourceGraph graph) : base(graph) { }
3737

38-
public override IEnumerable<T> BeforeDelete(HashSet<T> entities, ResourcePipeline pipeline) { return entities; }
38+
public override IEnumerable<T> BeforeDelete(IAffectedResources<T> affected, ResourcePipeline pipeline) { return affected; }
3939
public override void AfterDelete(HashSet<T> entities, ResourcePipeline pipeline, bool succeeded) { }
4040
}
4141

@@ -59,7 +59,7 @@ public class YetAnotherDummyResourceDefinition : ResourceDefinition<YetAnotherDu
5959
{
6060
public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder().AddResource<YetAnotherDummy>().Build()) { }
6161

62-
public override IEnumerable<YetAnotherDummy> BeforeDelete(HashSet<YetAnotherDummy> entities, ResourcePipeline pipeline) { return entities; }
62+
public override IEnumerable<YetAnotherDummy> BeforeDelete(IAffectedResources<YetAnotherDummy> affected, ResourcePipeline pipeline) { return affected; }
6363

6464
[LoadDatabaseValues(false)]
6565
public override void AfterDelete(HashSet<YetAnotherDummy> entities, ResourcePipeline pipeline, bool succeeded) { }
@@ -81,7 +81,7 @@ public class DoubleDummyResourceDefinition1 : ResourceDefinition<DoubleDummy>
8181
{
8282
public DoubleDummyResourceDefinition1() : base(new ResourceGraphBuilder().AddResource<DoubleDummy>().Build()) { }
8383

84-
public override IEnumerable<DoubleDummy> BeforeDelete(HashSet<DoubleDummy> entities, ResourcePipeline pipeline) { return entities; }
84+
public override IEnumerable<DoubleDummy> BeforeDelete(IAffectedResources<DoubleDummy> affected, ResourcePipeline pipeline) { return affected.Entities; }
8585
}
8686
public class DoubleDummyResourceDefinition2 : ResourceDefinition<DoubleDummy>
8787
{

0 commit comments

Comments
 (0)