Skip to content

Commit 0f41f85

Browse files
author
Bart Koelman
committed
Ported existing unit tests and changed how included[] is built.
It now always emits related resources in relationship declaration order, even in deeply nested circular chains where a subsequent inclusion chain is deeper and adds more relationships to an already converted resource. Performance impact, summary: - In the endpoint test, this commit improves performance for rendering includes, while slightly decreasing it for the other two scenarios. All are still faster or the same compared to the master branch. - In BenchmarkDotNet, this commit slightly increases rendering time, compared to earlier commits in this PR, but it is still faster than the master branch. Measurement results for GET http://localhost:14140/api/v1/todoItems?include=owner,assignee,tags: Write response body ............................. 0:00:00:00.0010385 -> 0:00:00:00.0008060 ... 77% (was: 130%) Measurement results for GET http://localhost:14140/api/v1/todoItems?filter=and(startsWith(description,'T'),equals(priority,'Low'),not(equals(owner,null)),not(equals(assignee,null))): Write response body ............................. 0:00:00:00.0006601 -> 0:00:00:00.0005629 ... 85% (was: 70%) Measurement results for POST http://localhost:14140/api/v1/operations (10x add-resource): Write response body ............................. 0:00:00:00.0003432 -> 0:00:00:00.0003411 ... 99% (was: 95%) | Method | Mean | Error | StdDev | |---------------------------------- |---------:|--------:|--------:| | LegacySerializeOperationsResponse | 239.0 us | 3.17 us | 2.81 us | | SerializeOperationsResponse | 153.8 us | 0.72 us | 0.60 us | | (new) SerializeOperationsResponse | 168.6 us | 1.74 us | 1.63 us | | Method | Mean | Error | StdDev | |-------------------------------- |---------:|--------:|--------:| | LegacySerializeResourceResponse | 177.6 us | 0.56 us | 0.50 us | | SerializeResourceResponse | 101.3 us | 0.31 us | 0.29 us | | (new) SerializeResourceResponse | 123.7 us | 1.12 us | 1.05 us |
1 parent 4a8bfdd commit 0f41f85

File tree

10 files changed

+1271
-187
lines changed

10 files changed

+1271
-187
lines changed

src/JsonApiDotNetCore/Serialization/Response/IncludedCollection.cs

Lines changed: 0 additions & 78 deletions
This file was deleted.
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Resources;
7+
using JsonApiDotNetCore.Resources.Annotations;
8+
using JsonApiDotNetCore.Serialization.Objects;
9+
10+
namespace JsonApiDotNetCore.Serialization.Response
11+
{
12+
/// <summary>
13+
/// Represents a dependency tree of resource objects. It provides the values for 'data' and 'included' in the response body. The tree is built by
14+
/// recursively walking the resource relationships from the inclusion chains. Note that a subsequent chain may add additional relationships to a resource
15+
/// object that was produced by an earlier chain. Afterwards, this tree is used to fill relationship objects in the resource objects (depending on sparse
16+
/// fieldsets) and to emit all entries in relationship declaration order.
17+
/// </summary>
18+
internal sealed class ResourceObjectTreeNode : IEquatable<ResourceObjectTreeNode>
19+
{
20+
// Placeholder root node for the tree, which is never emitted itself.
21+
private static readonly ResourceType RootType = new("(root)", typeof(object), typeof(object));
22+
private static readonly IIdentifiable RootResource = new EmptyResource();
23+
24+
// Direct children from root. These are emitted in 'data'.
25+
private List<ResourceObjectTreeNode> _directChildren;
26+
27+
// Related resource objects per relationship. These are emitted in 'included'.
28+
private Dictionary<RelationshipAttribute, HashSet<ResourceObjectTreeNode>> _childrenByRelationship;
29+
30+
private bool IsTreeRoot => RootType.Equals(Type);
31+
32+
// The resource this node was built for. We only store it for the LinkBuilder.
33+
public IIdentifiable Resource { get; }
34+
35+
// The resource type. We use its relationships to maintain order.
36+
public ResourceType Type { get; }
37+
38+
// The produced resource object from Resource. For each resource, at most one ResourceObject and one tree node must exist.
39+
public ResourceObject ResourceObject { get; }
40+
41+
public ResourceObjectTreeNode(IIdentifiable resource, ResourceType type, ResourceObject resourceObject)
42+
{
43+
ArgumentGuard.NotNull(resource, nameof(resource));
44+
ArgumentGuard.NotNull(type, nameof(type));
45+
ArgumentGuard.NotNull(resourceObject, nameof(resourceObject));
46+
47+
Resource = resource;
48+
Type = type;
49+
ResourceObject = resourceObject;
50+
}
51+
52+
public static ResourceObjectTreeNode CreateRoot()
53+
{
54+
return new ResourceObjectTreeNode(RootResource, RootType, new ResourceObject());
55+
}
56+
57+
public void AttachDirectChild(ResourceObjectTreeNode treeNode)
58+
{
59+
ArgumentGuard.NotNull(treeNode, nameof(treeNode));
60+
61+
_directChildren ??= new List<ResourceObjectTreeNode>();
62+
_directChildren.Add(treeNode);
63+
}
64+
65+
public void EnsureHasRelationship(RelationshipAttribute relationship)
66+
{
67+
ArgumentGuard.NotNull(relationship, nameof(relationship));
68+
69+
_childrenByRelationship ??= new Dictionary<RelationshipAttribute, HashSet<ResourceObjectTreeNode>>();
70+
71+
if (!_childrenByRelationship.ContainsKey(relationship))
72+
{
73+
_childrenByRelationship[relationship] = new HashSet<ResourceObjectTreeNode>();
74+
}
75+
}
76+
77+
public void AttachRelationshipChild(RelationshipAttribute relationship, ResourceObjectTreeNode rightNode)
78+
{
79+
ArgumentGuard.NotNull(relationship, nameof(relationship));
80+
ArgumentGuard.NotNull(rightNode, nameof(rightNode));
81+
82+
HashSet<ResourceObjectTreeNode> rightNodes = _childrenByRelationship[relationship];
83+
rightNodes.Add(rightNode);
84+
}
85+
86+
/// <summary>
87+
/// Recursively walks the tree and returns the set of unique nodes. Uses relationship declaration order.
88+
/// </summary>
89+
public ISet<ResourceObjectTreeNode> GetUniqueNodes()
90+
{
91+
AssertIsTreeRoot();
92+
93+
var visited = new HashSet<ResourceObjectTreeNode>();
94+
95+
VisitSubtree(this, visited);
96+
97+
return visited;
98+
}
99+
100+
private static void VisitSubtree(ResourceObjectTreeNode treeNode, ISet<ResourceObjectTreeNode> visited)
101+
{
102+
if (visited.Contains(treeNode))
103+
{
104+
return;
105+
}
106+
107+
if (!treeNode.IsTreeRoot)
108+
{
109+
visited.Add(treeNode);
110+
}
111+
112+
VisitDirectChildrenInSubtree(treeNode, visited);
113+
VisitRelationshipChildrenInSubtree(treeNode, visited);
114+
}
115+
116+
private static void VisitDirectChildrenInSubtree(ResourceObjectTreeNode treeNode, ISet<ResourceObjectTreeNode> visited)
117+
{
118+
if (treeNode._directChildren != null)
119+
{
120+
foreach (ResourceObjectTreeNode child in treeNode._directChildren)
121+
{
122+
VisitSubtree(child, visited);
123+
}
124+
}
125+
}
126+
127+
private static void VisitRelationshipChildrenInSubtree(ResourceObjectTreeNode treeNode, ISet<ResourceObjectTreeNode> visited)
128+
{
129+
if (treeNode._childrenByRelationship != null)
130+
{
131+
foreach (RelationshipAttribute relationship in treeNode.Type.Relationships)
132+
{
133+
if (treeNode._childrenByRelationship.TryGetValue(relationship, out HashSet<ResourceObjectTreeNode> rightNodes))
134+
{
135+
VisitRelationshipChildInSubtree(rightNodes, visited);
136+
}
137+
}
138+
}
139+
}
140+
141+
private static void VisitRelationshipChildInSubtree(HashSet<ResourceObjectTreeNode> rightNodes, ISet<ResourceObjectTreeNode> visited)
142+
{
143+
foreach (ResourceObjectTreeNode rightNode in rightNodes)
144+
{
145+
VisitSubtree(rightNode, visited);
146+
}
147+
}
148+
149+
public ISet<ResourceObjectTreeNode> GetRightNodesInRelationship(RelationshipAttribute relationship)
150+
{
151+
return _childrenByRelationship != null && _childrenByRelationship.TryGetValue(relationship, out HashSet<ResourceObjectTreeNode> rightNodes)
152+
? rightNodes
153+
: null;
154+
}
155+
156+
/// <summary>
157+
/// Provides the value for 'data' in the response body. Uses relationship declaration order.
158+
/// </summary>
159+
public IList<ResourceObject> GetResponseData()
160+
{
161+
AssertIsTreeRoot();
162+
163+
return GetDirectChildren().Select(child => child.ResourceObject).ToArray();
164+
}
165+
166+
/// <summary>
167+
/// Provides the value for 'included' in the response body. Uses relationship declaration order.
168+
/// </summary>
169+
public IList<ResourceObject> GetResponseIncluded()
170+
{
171+
AssertIsTreeRoot();
172+
173+
var visited = new HashSet<ResourceObjectTreeNode>();
174+
175+
foreach (ResourceObjectTreeNode child in GetDirectChildren())
176+
{
177+
VisitRelationshipChildrenInSubtree(child, visited);
178+
}
179+
180+
return visited.Select(node => node.ResourceObject).ToArray();
181+
}
182+
183+
private IList<ResourceObjectTreeNode> GetDirectChildren()
184+
{
185+
return _directChildren == null ? Array.Empty<ResourceObjectTreeNode>() : _directChildren;
186+
}
187+
188+
private void AssertIsTreeRoot()
189+
{
190+
if (!IsTreeRoot)
191+
{
192+
throw new InvalidOperationException("Internal error: this method should only be called from the root of the tree.");
193+
}
194+
}
195+
196+
public bool Equals(ResourceObjectTreeNode other)
197+
{
198+
if (ReferenceEquals(null, other))
199+
{
200+
return false;
201+
}
202+
203+
if (ReferenceEquals(this, other))
204+
{
205+
return true;
206+
}
207+
208+
return ResourceObjectComparer.Instance.Equals(ResourceObject, other.ResourceObject);
209+
}
210+
211+
public override bool Equals(object other)
212+
{
213+
return Equals(other as ResourceObjectTreeNode);
214+
}
215+
216+
public override int GetHashCode()
217+
{
218+
return ResourceObject.GetHashCode();
219+
}
220+
221+
public override string ToString()
222+
{
223+
var builder = new StringBuilder();
224+
builder.Append(IsTreeRoot ? Type.PublicName : $"{ResourceObject.Type}:{ResourceObject.Id}");
225+
226+
if (_directChildren != null)
227+
{
228+
builder.Append($", children: {_directChildren.Count}");
229+
}
230+
else if (_childrenByRelationship != null)
231+
{
232+
builder.Append($", children: {string.Join(',', _childrenByRelationship.Select(pair => $"{pair.Key.PublicName} ({pair.Value.Count})"))}");
233+
}
234+
235+
return builder.ToString();
236+
}
237+
238+
private sealed class EmptyResource : IIdentifiable
239+
{
240+
public string StringId { get; set; }
241+
public string LocalId { get; set; }
242+
}
243+
244+
private sealed class ResourceObjectComparer : IEqualityComparer<ResourceObject>
245+
{
246+
public static readonly ResourceObjectComparer Instance = new();
247+
248+
private ResourceObjectComparer()
249+
{
250+
}
251+
252+
public bool Equals(ResourceObject x, ResourceObject y)
253+
{
254+
if (ReferenceEquals(x, y))
255+
{
256+
return true;
257+
}
258+
259+
if (x is null || y is null || x.GetType() != y.GetType())
260+
{
261+
return false;
262+
}
263+
264+
return x.Type == y.Type && x.Id == y.Id && x.Lid == y.Lid;
265+
}
266+
267+
public int GetHashCode(ResourceObject obj)
268+
{
269+
return HashCode.Combine(obj.Type, obj.Id, obj.Lid);
270+
}
271+
}
272+
}
273+
}

0 commit comments

Comments
 (0)