Skip to content

Commit 41df412

Browse files
author
Bart Koelman
committed
Refactored include chains into a tree, so it can be used from ResourceDefinitions
1 parent a6ce7ff commit 41df412

File tree

19 files changed

+408
-73
lines changed

19 files changed

+408
-73
lines changed

docs/usage/resources/resource-definitions.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ public class AccountDefinition : ResourceDefinition<Account>
144144
}
145145
```
146146

147+
## Block including related resources
148+
149+
```c#
150+
public class EmployeeDefinition : ResourceDefinition<Employee>
151+
{
152+
public override IReadOnlyCollection<IncludeElementExpression> OnApplyIncludes(IReadOnlyCollection<IncludeElementExpression> existingIncludes)
153+
{
154+
if (existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Employee.Manager)))
155+
{
156+
throw new JsonApiException(new Error(HttpStatusCode.BadRequest)
157+
{
158+
Title = "Including the manager of employees is not permitted."
159+
});
160+
}
161+
162+
return existingIncludes;
163+
}
164+
}
165+
```
166+
147167
## Custom query string parameters
148168

149169
_since v3.0.0_

src/JsonApiDotNetCore/Hooks/Execution/HookExecutorHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ private IEnumerable<TResource> GetWhereAndInclude<TResource, TId>(IEnumerable<TI
147147
var chains = relationshipsToNextLayer.Select(relationship => new ResourceFieldChainExpression(relationship)).ToList();
148148
if (chains.Any())
149149
{
150-
queryLayer.Include = new IncludeExpression(chains);
150+
queryLayer.Include = IncludeChainConverter.FromRelationshipChains(chains);
151151
}
152152

153153
var repository = GetRepository<TResource, TId>();

src/JsonApiDotNetCore/Hooks/ResourceHookExecutor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public void BeforeRead<TResource>(ResourcePipeline pipeline, string stringId = n
5555
.OfType<IncludeExpression>()
5656
.ToArray();
5757

58-
foreach (var chain in includes.SelectMany(include => include.Chains))
58+
foreach (var chain in includes.SelectMany(IncludeChainConverter.GetRelationshipChains))
5959
{
6060
RecursiveBeforeRead(chain.Fields.Cast<RelationshipAttribute>().ToList(), pipeline, calledContainers);
6161
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using JsonApiDotNetCore.Models.Annotation;
5+
6+
namespace JsonApiDotNetCore.Internal.Queries.Expressions
7+
{
8+
public static class IncludeChainConverter
9+
{
10+
/// <summary>
11+
/// Converts a tree of inclusions into a set of relationship chains.
12+
/// </summary>
13+
/// <example>
14+
/// Input tree:
15+
/// Article
16+
/// {
17+
/// Blog,
18+
/// Revisions
19+
/// {
20+
/// Author
21+
/// }
22+
/// }
23+
///
24+
/// Output chains:
25+
/// Article -> Blog,
26+
/// Article -> Revisions -> Author
27+
/// </example>
28+
public static IReadOnlyCollection<ResourceFieldChainExpression> GetRelationshipChains(IncludeExpression include)
29+
{
30+
if (include == null)
31+
{
32+
throw new ArgumentNullException(nameof(include));
33+
}
34+
35+
IncludeToChainsConverter converter = new IncludeToChainsConverter();
36+
converter.Visit(include, null);
37+
38+
return converter.Chains.AsReadOnly();
39+
}
40+
41+
/// <summary>
42+
/// Converts a set of relationship chains into a tree of inclusions.
43+
/// </summary>
44+
/// <example>
45+
/// Input chains:
46+
/// Article -> Blog,
47+
/// Article -> Revisions -> Author
48+
///
49+
/// Output tree:
50+
/// Article
51+
/// {
52+
/// Blog,
53+
/// Revisions
54+
/// {
55+
/// Author
56+
/// }
57+
/// }
58+
/// </example>
59+
public static IncludeExpression FromRelationshipChains(IEnumerable<ResourceFieldChainExpression> chains)
60+
{
61+
if (chains == null)
62+
{
63+
throw new ArgumentNullException(nameof(chains));
64+
}
65+
66+
var elements = ConvertChainsToElements(chains);
67+
return elements.Any() ? new IncludeExpression(elements) : IncludeExpression.Empty;
68+
}
69+
70+
private static IReadOnlyCollection<IncludeElementExpression> ConvertChainsToElements(IEnumerable<ResourceFieldChainExpression> chains)
71+
{
72+
var rootNode = new MutableIncludeNode(null);
73+
74+
foreach (ResourceFieldChainExpression chain in chains)
75+
{
76+
MutableIncludeNode currentNode = rootNode;
77+
78+
foreach (var relationship in chain.Fields.OfType<RelationshipAttribute>())
79+
{
80+
if (!currentNode.Children.ContainsKey(relationship))
81+
{
82+
currentNode.Children[relationship] = new MutableIncludeNode(relationship);
83+
}
84+
85+
currentNode = currentNode.Children[relationship];
86+
}
87+
}
88+
89+
return rootNode.Children.Values.Select(child => child.ToExpression()).ToList();
90+
}
91+
92+
private sealed class IncludeToChainsConverter : QueryExpressionVisitor<object, object>
93+
{
94+
private readonly Stack<RelationshipAttribute> _parentRelationshipStack = new Stack<RelationshipAttribute>();
95+
96+
public List<ResourceFieldChainExpression> Chains { get; } = new List<ResourceFieldChainExpression>();
97+
98+
public override object VisitInclude(IncludeExpression expression, object argument)
99+
{
100+
foreach (IncludeElementExpression element in expression.Elements)
101+
{
102+
Visit(element, null);
103+
}
104+
105+
return null;
106+
}
107+
108+
public override object VisitIncludeElement(IncludeElementExpression expression, object argument)
109+
{
110+
if (!expression.Children.Any())
111+
{
112+
FlushChain(expression);
113+
}
114+
else
115+
{
116+
_parentRelationshipStack.Push(expression.Relationship);
117+
118+
foreach (IncludeElementExpression child in expression.Children)
119+
{
120+
Visit(child, null);
121+
}
122+
123+
_parentRelationshipStack.Pop();
124+
}
125+
126+
return null;
127+
}
128+
129+
private void FlushChain(IncludeElementExpression expression)
130+
{
131+
List<RelationshipAttribute> fieldsInChain = _parentRelationshipStack.Reverse().ToList();
132+
fieldsInChain.Add(expression.Relationship);
133+
134+
Chains.Add(new ResourceFieldChainExpression(fieldsInChain));
135+
}
136+
}
137+
138+
private sealed class MutableIncludeNode
139+
{
140+
private readonly RelationshipAttribute _relationship;
141+
142+
public IDictionary<RelationshipAttribute, MutableIncludeNode> Children { get; } = new Dictionary<RelationshipAttribute, MutableIncludeNode>();
143+
144+
public MutableIncludeNode(RelationshipAttribute relationship)
145+
{
146+
_relationship = relationship;
147+
}
148+
149+
public IncludeElementExpression ToExpression()
150+
{
151+
var elementChildren = Children.Values.Select(child => child.ToExpression()).ToList();
152+
return new IncludeElementExpression(_relationship, elementChildren);
153+
}
154+
}
155+
}
156+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using JsonApiDotNetCore.Models.Annotation;
6+
7+
namespace JsonApiDotNetCore.Internal.Queries.Expressions
8+
{
9+
public class IncludeElementExpression : QueryExpression
10+
{
11+
public RelationshipAttribute Relationship { get; }
12+
public IReadOnlyCollection<IncludeElementExpression> Children { get; }
13+
14+
public IncludeElementExpression(RelationshipAttribute relationship)
15+
: this(relationship, Array.Empty<IncludeElementExpression>())
16+
{
17+
}
18+
19+
public IncludeElementExpression(RelationshipAttribute relationship, IReadOnlyCollection<IncludeElementExpression> children)
20+
{
21+
Relationship = relationship ?? throw new ArgumentNullException(nameof(relationship));
22+
Children = children ?? throw new ArgumentNullException(nameof(children));
23+
}
24+
25+
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
26+
{
27+
return visitor.VisitIncludeElement(this, argument);
28+
}
29+
30+
public override string ToString()
31+
{
32+
var builder = new StringBuilder();
33+
builder.Append(Relationship);
34+
35+
if (Children.Any())
36+
{
37+
builder.Append('{');
38+
builder.Append(string.Join(",", Children.Select(child => child.ToString())));
39+
builder.Append('}');
40+
}
41+
42+
return builder.ToString();
43+
}
44+
}
45+
}

src/JsonApiDotNetCore/Internal/Queries/Expressions/IncludeExpression.cs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,22 @@ namespace JsonApiDotNetCore.Internal.Queries.Expressions
66
{
77
public class IncludeExpression : QueryExpression
88
{
9-
// TODO: Unfold into a tree of child relationships, so it can be used from ResourceDefinitions.
10-
11-
public IReadOnlyCollection<ResourceFieldChainExpression> Chains { get; }
9+
public IReadOnlyCollection<IncludeElementExpression> Elements { get; }
1210

1311
public static readonly IncludeExpression Empty = new IncludeExpression();
1412

1513
private IncludeExpression()
1614
{
17-
Chains = Array.Empty<ResourceFieldChainExpression>();
15+
Elements = Array.Empty<IncludeElementExpression>();
1816
}
1917

20-
public IncludeExpression(IReadOnlyCollection<ResourceFieldChainExpression> chains)
18+
public IncludeExpression(IReadOnlyCollection<IncludeElementExpression> elements)
2119
{
22-
Chains = chains ?? throw new ArgumentNullException(nameof(chains));
20+
Elements = elements ?? throw new ArgumentNullException(nameof(elements));
2321

24-
if (!chains.Any())
22+
if (!elements.Any())
2523
{
26-
throw new ArgumentException("Must have one or more chains.", nameof(chains));
24+
throw new ArgumentException("Must have one or more elements.", nameof(elements));
2725
}
2826
}
2927

@@ -34,7 +32,8 @@ public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgum
3432

3533
public override string ToString()
3634
{
37-
return string.Join(",", Chains.Select(child => child.ToString()));
35+
var chains = IncludeChainConverter.GetRelationshipChains(this);
36+
return string.Join(",", chains.Select(child => child.ToString()));
3837
}
3938
}
4039
}

src/JsonApiDotNetCore/Internal/Queries/Expressions/QueryExpressionVisitor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ public virtual TResult VisitInclude(IncludeExpression expression, TArgument argu
102102
return DefaultVisit(expression, argument);
103103
}
104104

105+
public virtual TResult VisitIncludeElement(IncludeElementExpression expression, TArgument argument)
106+
{
107+
return DefaultVisit(expression, argument);
108+
}
109+
105110
public virtual TResult VisitQueryableHandler(QueryableHandlerExpression expression, TArgument argument)
106111
{
107112
return DefaultVisit(expression, argument);

src/JsonApiDotNetCore/Internal/Queries/Parsing/IncludeParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected IncludeExpression ParseInclude()
3838
chains.Add(nextChain);
3939
}
4040

41-
return new IncludeExpression(chains);
41+
return IncludeChainConverter.FromRelationshipChains(chains);
4242
}
4343
}
4444
}

0 commit comments

Comments
 (0)