Skip to content

Commit c6762b3

Browse files
authored
Merge pull request #541 from json-api-dotnet/feat/fluent-get-affected
GetAffected syntax in resource hook helpers
2 parents 455983f + 3148d9f commit c6762b3

File tree

12 files changed

+241
-73
lines changed

12 files changed

+241
-73
lines changed

src/Examples/GettingStarted/Properties/launchSettings.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
},
1818
"GettingStarted": {
1919
"commandName": "Project",
20-
"launchBrowser": true
20+
"launchBrowser": true,
21+
"environmentVariables": {}
2122
}
2223
}
23-
}
24+
}

src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@
2525
}
2626
}
2727
}
28-
}
28+
}

src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
}
1717
},
1818
"NoEntityFrameworkExample": {
19-
"commandName": "Project"
19+
"commandName": "Project",
20+
"environmentVariables": {}
2021
}
2122
}
2223
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
{
2-
"iisSettings": {
3-
"windowsAuthentication": false,
4-
"anonymousAuthentication": true,
5-
"iisExpress": {
6-
"applicationUrl": "http://localhost:57181/",
7-
"sslPort": 0
8-
}
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:57181/",
7+
"sslPort": 0
8+
}
9+
},
10+
"profiles": {
11+
"IIS Express": {
12+
"commandName": "IISExpress",
13+
"launchBrowser": true,
14+
"launchUrl": "api/v1/students",
15+
"environmentVariables": {
16+
"ASPNETCORE_ENVIRONMENT": "Development"
17+
}
918
},
10-
"profiles": {
11-
"IIS Express": {
12-
"commandName": "IISExpress",
13-
"launchBrowser": true,
14-
"launchUrl": "api/v1/students",
15-
"environmentVariables": {
16-
"ASPNETCORE_ENVIRONMENT": "Development"
17-
}
18-
},
19-
"ResourceEntitySeparationExample": {
20-
"commandName": "Project"
21-
}
19+
"ResourceEntitySeparationExample": {
20+
"commandName": "Project",
21+
"environmentVariables": {}
2222
}
23-
}
23+
}
24+
}

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

+12-11
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public static IServiceCollection AddJsonApi(
6464
var config = new JsonApiOptions();
6565
configureOptions(config);
6666

67-
if(autoDiscover != null)
67+
if (autoDiscover != null)
6868
{
6969
var facade = new ServiceDiscoveryFacade(services, config.ResourceGraphBuilder);
7070
autoDiscover(facade);
@@ -159,7 +159,7 @@ public static void AddJsonApiInternals(
159159
services.AddScoped<IControllerContext, Services.ControllerContext>();
160160
services.AddScoped<IDocumentBuilderOptionsProvider, DocumentBuilderOptionsProvider>();
161161

162-
162+
163163
if (jsonApiOptions.EnableResourceHooks)
164164
{
165165
services.AddSingleton(typeof(IHooksDiscovery<>), typeof(HooksDiscovery<>));
@@ -204,7 +204,7 @@ public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions js
204204
/// Adds all required registrations for the service to the container
205205
/// </summary>
206206
/// <exception cref="JsonApiSetupException"/>
207-
public static IServiceCollection AddResourceService<T>(this IServiceCollection services)
207+
public static IServiceCollection AddResourceService<T>(this IServiceCollection services)
208208
{
209209
var typeImplementsAnExpectedInterface = false;
210210

@@ -213,28 +213,29 @@ public static IServiceCollection AddResourceService<T>(this IServiceCollection s
213213
// it is _possible_ that a single concrete type could be used for multiple resources...
214214
var resourceDescriptors = GetResourceTypesFromServiceImplementation(serviceImplementationType);
215215

216-
foreach(var resourceDescriptor in resourceDescriptors)
216+
foreach (var resourceDescriptor in resourceDescriptors)
217217
{
218-
foreach(var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces)
218+
foreach (var openGenericType in ServiceDiscoveryFacade.ServiceInterfaces)
219219
{
220220
// A shorthand interface is one where the id type is ommitted
221221
// e.g. IResourceService<T> is the shorthand for IResourceService<T, TId>
222222
var isShorthandInterface = (openGenericType.GetTypeInfo().GenericTypeParameters.Length == 1);
223-
if(isShorthandInterface && resourceDescriptor.IdType != typeof(int))
223+
if (isShorthandInterface && resourceDescriptor.IdType != typeof(int))
224224
continue; // we can't create a shorthand for id types other than int
225225

226226
var concreteGenericType = isShorthandInterface
227227
? openGenericType.MakeGenericType(resourceDescriptor.ResourceType)
228228
: openGenericType.MakeGenericType(resourceDescriptor.ResourceType, resourceDescriptor.IdType);
229229

230-
if(concreteGenericType.IsAssignableFrom(serviceImplementationType)) {
230+
if (concreteGenericType.IsAssignableFrom(serviceImplementationType))
231+
{
231232
services.AddScoped(concreteGenericType, serviceImplementationType);
232233
typeImplementsAnExpectedInterface = true;
233234
}
234235
}
235236
}
236237

237-
if(typeImplementsAnExpectedInterface == false)
238+
if (typeImplementsAnExpectedInterface == false)
238239
throw new JsonApiSetupException($"{serviceImplementationType} does not implement any of the expected JsonApiDotNetCore interfaces.");
239240

240241
return services;
@@ -244,12 +245,12 @@ private static HashSet<ResourceDescriptor> GetResourceTypesFromServiceImplementa
244245
{
245246
var resourceDecriptors = new HashSet<ResourceDescriptor>();
246247
var interfaces = type.GetInterfaces();
247-
foreach(var i in interfaces)
248+
foreach (var i in interfaces)
248249
{
249-
if(i.IsGenericType)
250+
if (i.IsGenericType)
250251
{
251252
var firstGenericArgument = i.GenericTypeArguments.FirstOrDefault();
252-
if(TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == true)
253+
if (TypeLocator.TryGetResourceDescriptor(firstGenericArgument, out var resourceDescriptor) == true)
253254
{
254255
resourceDecriptors.Add(resourceDescriptor);
255256
}

src/JsonApiDotNetCore/Hooks/Execution/DiffableEntityHashSet.cs

+32-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
7+
using JsonApiDotNetCore.Extensions;
58
using JsonApiDotNetCore.Internal;
69
using JsonApiDotNetCore.Models;
10+
using JsonApiDotNetCore.Services;
711

812
namespace JsonApiDotNetCore.Hooks
913
{
@@ -21,31 +25,37 @@ public interface IDiffableEntityHashSet<TResource> : IEntityHashSet<TResource> w
2125
/// with their associated current value from the database.
2226
/// </summary>
2327
IEnumerable<EntityDiffPair<TResource>> GetDiffs();
24-
28+
2529
}
2630

2731
/// <inheritdoc />
2832
public class DiffableEntityHashSet<TResource> : EntityHashSet<TResource>, IDiffableEntityHashSet<TResource> where TResource : class, IIdentifiable
2933
{
3034
private readonly HashSet<TResource> _databaseValues;
3135
private readonly bool _databaseValuesLoaded;
36+
private Dictionary<PropertyInfo, HashSet<TResource>> _updatedAttributes;
3237

3338
public DiffableEntityHashSet(HashSet<TResource> requestEntities,
3439
HashSet<TResource> databaseEntities,
35-
Dictionary<RelationshipAttribute, HashSet<TResource>> relationships)
40+
Dictionary<RelationshipAttribute, HashSet<TResource>> relationships,
41+
Dictionary<PropertyInfo, HashSet<TResource>> updatedAttributes)
3642
: base(requestEntities, relationships)
3743
{
3844
_databaseValues = databaseEntities;
3945
_databaseValuesLoaded |= _databaseValues != null;
46+
_updatedAttributes = updatedAttributes;
4047
}
4148

4249
/// <summary>
4350
/// Used internally by the ResourceHookExecutor to make live a bit easier with generics
4451
/// </summary>
4552
internal DiffableEntityHashSet(IEnumerable requestEntities,
4653
IEnumerable databaseEntities,
47-
Dictionary<RelationshipAttribute, IEnumerable> relationships)
48-
: this((HashSet<TResource>)requestEntities, (HashSet<TResource>)databaseEntities, TypeHelper.ConvertRelationshipDictionary<TResource>(relationships)) { }
54+
Dictionary<RelationshipAttribute, IEnumerable> relationships,
55+
IJsonApiContext jsonApiContext)
56+
: this((HashSet<TResource>)requestEntities, (HashSet<TResource>)databaseEntities, TypeHelper.ConvertRelationshipDictionary<TResource>(relationships),
57+
TypeHelper.ConvertAttributeDictionary(jsonApiContext.AttributesToUpdate, (HashSet<TResource>)requestEntities))
58+
{ }
4959

5060

5161
/// <inheritdoc />
@@ -60,6 +70,24 @@ public IEnumerable<EntityDiffPair<TResource>> GetDiffs()
6070
}
6171
}
6272

73+
/// <inheritdoc />
74+
public new HashSet<TResource> GetAffected(Expression<Func<TResource, object>> NavigationAction)
75+
{
76+
var propertyInfo = TypeHelper.ParseNavigationExpression(NavigationAction);
77+
var propertyType = propertyInfo.PropertyType;
78+
if (propertyType.Inherits(typeof(IEnumerable))) propertyType = TypeHelper.GetTypeOfList(propertyType);
79+
if (propertyType.Implements<IIdentifiable>())
80+
{
81+
// the navigation action references a relationship. Redirect the call to the relationship dictionary.
82+
return base.GetAffected(NavigationAction);
83+
}
84+
else if (_updatedAttributes.TryGetValue(propertyInfo, out HashSet<TResource> entities))
85+
{
86+
return entities;
87+
}
88+
return new HashSet<TResource>();
89+
}
90+
6391
private HashSet<TResource> ThrowNoDbValuesError()
6492
{
6593
throw new MemberAccessException("Cannot iterate over the diffs if the LoadDatabaseValues option is set to false");

src/JsonApiDotNetCore/Hooks/Execution/EntityHashSet.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System;
66
using System.Collections.ObjectModel;
77
using System.Collections.Immutable;
8+
using System.Linq.Expressions;
89

910
namespace JsonApiDotNetCore.Hooks
1011
{
@@ -26,7 +27,7 @@ public interface IEntityHashSet<TResource> : IByAffectedRelationships<TResource>
2627
/// </summary>
2728
public class EntityHashSet<TResource> : HashSet<TResource>, IEntityHashSet<TResource> where TResource : class, IIdentifiable
2829
{
29-
30+
3031

3132
/// <inheritdoc />
3233
public Dictionary<RelationshipAttribute, HashSet<TResource>> AffectedRelationships { get => _relationships; }
@@ -53,9 +54,15 @@ public Dictionary<RelationshipAttribute, HashSet<TResource>> GetByRelationship(T
5354
}
5455

5556
/// <inheritdoc />
56-
public Dictionary<RelationshipAttribute, HashSet<TResource>> GetByRelationship<TRelatedResource>() where TRelatedResource : class, IIdentifiable
57+
public Dictionary<RelationshipAttribute, HashSet<TResource>> GetByRelationship<TRelatedResource>() where TRelatedResource : class, IIdentifiable
5758
{
5859
return GetByRelationship(typeof(TRelatedResource));
5960
}
61+
62+
/// <inheritdoc />
63+
public HashSet<TResource> GetAffected(Expression<Func<TResource, object>> NavigationAction)
64+
{
65+
return _relationships.GetAffected(NavigationAction);
66+
}
6067
}
6168
}

src/JsonApiDotNetCore/Hooks/Execution/RelationshipsDictionary.cs

+25-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Collections;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
57
using JsonApiDotNetCore.Internal;
68
using JsonApiDotNetCore.Models;
79

@@ -15,13 +17,9 @@ public interface IRelationshipsDictionary { }
1517
/// <summary>
1618
/// An interface that is implemented to expose a relationship dictionary on another class.
1719
/// </summary>
18-
public interface IByAffectedRelationships<TDependentResource> :
20+
public interface IByAffectedRelationships<TDependentResource> :
1921
IRelationshipGetters<TDependentResource> where TDependentResource : class, IIdentifiable
2022
{
21-
/// todo: expose getters that behave something like this:
22-
/// relationshipDictionary.GetAffected( entity => entity.NavigationProperty ).
23-
/// see https://stackoverflow.com/a/17116267/4441216
24-
2523
/// <summary>
2624
/// Gets a dictionary of affected resources grouped by affected relationships.
2725
/// </summary>
@@ -31,10 +29,11 @@ public interface IByAffectedRelationships<TDependentResource> :
3129
/// <summary>
3230
/// A helper class that provides insights in which relationships have been updated for which entities.
3331
/// </summary>
34-
public interface IRelationshipsDictionary<TDependentResource> :
35-
IRelationshipGetters<TDependentResource>,
36-
IReadOnlyDictionary<RelationshipAttribute, HashSet<TDependentResource>>,
37-
IRelationshipsDictionary where TDependentResource : class, IIdentifiable { }
32+
public interface IRelationshipsDictionary<TDependentResource> :
33+
IRelationshipGetters<TDependentResource>,
34+
IReadOnlyDictionary<RelationshipAttribute, HashSet<TDependentResource>>,
35+
IRelationshipsDictionary where TDependentResource : class, IIdentifiable
36+
{ }
3837

3938
/// <summary>
4039
/// A helper class that provides insights in which relationships have been updated for which entities.
@@ -49,16 +48,24 @@ public interface IRelationshipGetters<TResource> where TResource : class, IIdent
4948
/// Gets a dictionary of all entities that have an affected relationship to type <paramref name="principalType"/>
5049
/// </summary>
5150
Dictionary<RelationshipAttribute, HashSet<TResource>> GetByRelationship(Type relatedResourceType);
51+
52+
/// <summary>
53+
/// Gets a collection of all the entities for the property within <paramref name="NavigationAction"/>
54+
/// has been affected by the request
55+
/// </summary>
56+
/// <param name="NavigationAction"></param>
57+
HashSet<TResource> GetAffected(Expression<Func<TResource, object>> NavigationAction);
5258
}
5359

60+
5461
/// <summary>
5562
/// Implementation of IAffectedRelationships{TDependentResource}
5663
///
5764
/// It is practically a ReadOnlyDictionary{RelationshipAttribute, HashSet{TDependentResource}} dictionary
5865
/// with the two helper methods defined on IAffectedRelationships{TDependentResource}.
5966
/// </summary>
6067
public class RelationshipsDictionary<TResource> :
61-
Dictionary<RelationshipAttribute, HashSet<TResource>>,
68+
Dictionary<RelationshipAttribute, HashSet<TResource>>,
6269
IRelationshipsDictionary<TResource> where TResource : class, IIdentifiable
6370
{
6471
/// <summary>
@@ -70,7 +77,7 @@ public RelationshipsDictionary(Dictionary<RelationshipAttribute, HashSet<TResour
7077
/// <summary>
7178
/// Used internally by the ResourceHookExecutor to make live a bit easier with generics
7279
/// </summary>
73-
internal RelationshipsDictionary(Dictionary<RelationshipAttribute, IEnumerable> relationships)
80+
internal RelationshipsDictionary(Dictionary<RelationshipAttribute, IEnumerable> relationships)
7481
: this(TypeHelper.ConvertRelationshipDictionary<TResource>(relationships)) { }
7582

7683
/// <inheritdoc />
@@ -84,5 +91,12 @@ public Dictionary<RelationshipAttribute, HashSet<TResource>> GetByRelationship(T
8491
{
8592
return this.Where(p => p.Key.DependentType == relatedType).ToDictionary(p => p.Key, p => p.Value);
8693
}
94+
95+
/// <inheritdoc />
96+
public HashSet<TResource> GetAffected(Expression<Func<TResource, object>> NavigationAction)
97+
{
98+
var property = TypeHelper.ParseNavigationExpression(NavigationAction);
99+
return this.Where(p => p.Key.InternalRelationshipName == property.Name).Select(p => p.Value).SingleOrDefault();
100+
}
87101
}
88102
}

src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public virtual IEnumerable<TEntity> BeforeUpdate<TEntity>(IEnumerable<TEntity> e
4949
{
5050
var relationships = node.RelationshipsToNextLayer.Select(p => p.Attribute).ToArray();
5151
var dbValues = LoadDbValues(typeof(TEntity), (IEnumerable<TEntity>)node.UniqueEntities, ResourceHook.BeforeUpdate, relationships);
52-
var diff = new DiffableEntityHashSet<TEntity>(node.UniqueEntities, dbValues, node.PrincipalsToNextLayer());
52+
var diff = new DiffableEntityHashSet<TEntity>(node.UniqueEntities, dbValues, node.PrincipalsToNextLayer(), _context);
5353
IEnumerable<TEntity> updated = container.BeforeUpdate(diff, pipeline);
5454
node.UpdateUnique(updated);
5555
node.Reassign(entities);

0 commit comments

Comments
 (0)