Skip to content

Commit 67e0739

Browse files
author
Bart Koelman
authored
Fixed including extra resources from OnApplyIncudes (#1011)
* Refactored existing tests to use friendlier models * Makes the serializer take changes from `IResourceDefinition.OnApplyChanges` into account. This enables adding extra includes from a resource definition. This commit makes `ResponseResourceObjectBuilder` a bit faster too, because it no longer evaluates all constraints on each include. * Improved hit tracking assertions for resource definition callbacks. Optimization: Removed callback invocations for to-one include (see QueryLayerComposer) because their results are never used. * For convenience, expose the resource context that matches TResource in JsonApiResourceDefinition.
1 parent f145564 commit 67e0739

File tree

59 files changed

+1876
-958
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1876
-958
lines changed

src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ public void ConfigureServiceContainer(ICollection<Type> dbContextTypes)
158158
_services.AddScoped<IGenericServiceFactory, GenericServiceFactory>();
159159
_services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>));
160160
_services.AddScoped<IPaginationContext, PaginationContext>();
161+
_services.AddScoped<IEvaluatedIncludeCache, EvaluatedIncludeCache>();
161162
_services.AddScoped<IQueryLayerComposer, QueryLayerComposer>();
162163
_services.AddScoped<IInverseNavigationResolver, InverseNavigationResolver>();
163164
}

src/JsonApiDotNetCore/Queries/Expressions/IncludeChainConverter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using JsonApiDotNetCore.Resources.Annotations;
@@ -32,6 +33,11 @@ public IReadOnlyCollection<ResourceFieldChainExpression> GetRelationshipChains(I
3233
{
3334
ArgumentGuard.NotNull(include, nameof(include));
3435

36+
if (!include.Elements.Any())
37+
{
38+
return Array.Empty<ResourceFieldChainExpression>();
39+
}
40+
3541
var converter = new IncludeToChainsConverter();
3642
converter.Visit(include, null);
3743

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using JsonApiDotNetCore.Queries.Expressions;
2+
3+
namespace JsonApiDotNetCore.Queries.Internal
4+
{
5+
/// <inheritdoc />
6+
internal sealed class EvaluatedIncludeCache : IEvaluatedIncludeCache
7+
{
8+
private IncludeExpression _include;
9+
10+
/// <inheritdoc />
11+
public void Set(IncludeExpression include)
12+
{
13+
_include = include;
14+
}
15+
16+
/// <inheritdoc />
17+
public IncludeExpression Get()
18+
{
19+
return _include;
20+
}
21+
}
22+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using JsonApiDotNetCore.Queries.Expressions;
2+
using JsonApiDotNetCore.Resources;
3+
4+
namespace JsonApiDotNetCore.Queries.Internal
5+
{
6+
/// <summary>
7+
/// Provides in-memory storage for the evaluated inclusion tree within a request. This tree is produced from query string and resource definition
8+
/// callbacks. The cache enables the serialization layer to take changes from <see cref="IResourceDefinition{TResource,TId}.OnApplyIncludes" /> into
9+
/// account.
10+
/// </summary>
11+
public interface IEvaluatedIncludeCache
12+
{
13+
/// <summary>
14+
/// Stores the evaluated inclusion tree for later usage.
15+
/// </summary>
16+
void Set(IncludeExpression include);
17+
18+
/// <summary>
19+
/// Gets the evaluated inclusion tree that was stored earlier.
20+
/// </summary>
21+
IncludeExpression Get();
22+
}
23+
}

src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,28 @@ public class QueryLayerComposer : IQueryLayerComposer
2020
private readonly IJsonApiOptions _options;
2121
private readonly IPaginationContext _paginationContext;
2222
private readonly ITargetedFields _targetedFields;
23+
private readonly IEvaluatedIncludeCache _evaluatedIncludeCache;
2324
private readonly SparseFieldSetCache _sparseFieldSetCache;
2425

2526
public QueryLayerComposer(IEnumerable<IQueryConstraintProvider> constraintProviders, IResourceContextProvider resourceContextProvider,
2627
IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext,
27-
ITargetedFields targetedFields)
28+
ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache)
2829
{
2930
ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders));
3031
ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider));
3132
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
3233
ArgumentGuard.NotNull(options, nameof(options));
3334
ArgumentGuard.NotNull(paginationContext, nameof(paginationContext));
3435
ArgumentGuard.NotNull(targetedFields, nameof(targetedFields));
36+
ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache));
3537

3638
_constraintProviders = constraintProviders;
3739
_resourceContextProvider = resourceContextProvider;
3840
_resourceDefinitionAccessor = resourceDefinitionAccessor;
3941
_options = options;
4042
_paginationContext = paginationContext;
4143
_targetedFields = targetedFields;
44+
_evaluatedIncludeCache = evaluatedIncludeCache;
4245
_sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor);
4346
}
4447

@@ -72,6 +75,8 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource)
7275
QueryLayer topLayer = ComposeTopLayer(constraints, requestResource);
7376
topLayer.Include = ComposeChildren(topLayer, constraints);
7477

78+
_evaluatedIncludeCache.Set(topLayer.Include);
79+
7580
return topLayer;
7681
}
7782

@@ -159,12 +164,15 @@ private IReadOnlyCollection<IncludeElementExpression> ProcessIncludeSet(IReadOnl
159164
// @formatter:wrap_chained_method_calls restore
160165

161166
ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType);
167+
bool isToManyRelationship = includeElement.Relationship is HasManyAttribute;
162168

163169
var child = new QueryLayer(resourceContext)
164170
{
165-
Filter = GetFilter(expressionsInCurrentScope, resourceContext),
166-
Sort = GetSort(expressionsInCurrentScope, resourceContext),
167-
Pagination = ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceContext),
171+
Filter = isToManyRelationship ? GetFilter(expressionsInCurrentScope, resourceContext) : null,
172+
Sort = isToManyRelationship ? GetSort(expressionsInCurrentScope, resourceContext) : null,
173+
Pagination = isToManyRelationship
174+
? ((JsonApiOptions)_options).DisableChildrenPagination ? null : GetPagination(expressionsInCurrentScope, resourceContext)
175+
: null,
168176
Projection = GetProjectionForSparseAttributeSet(resourceContext)
169177
};
170178

src/JsonApiDotNetCore/QueryStrings/Internal/ResourceDefinitionQueryableParameterReader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public virtual void Read(string parameterName, StringValues parameterValue)
5656

5757
private object GetQueryableHandler(string parameterName)
5858
{
59-
Type resourceType = _request.PrimaryResource.ResourceType;
59+
Type resourceType = (_request.SecondaryResource ?? _request.PrimaryResource).ResourceType;
6060
object handler = _resourceDefinitionAccessor.GetQueryableHandlerForQueryStringParameter(resourceType, parameterName);
6161

6262
if (handler != null && _request.Kind != EndpointKind.Primary)

src/JsonApiDotNetCore/Resources/JsonApiResourceDefinition.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,17 @@ public class JsonApiResourceDefinition<TResource, TId> : IResourceDefinition<TRe
3737
{
3838
protected IResourceGraph ResourceGraph { get; }
3939

40+
/// <summary>
41+
/// Provides metadata for the resource type <typeparamref name="TResource" />.
42+
/// </summary>
43+
protected ResourceContext ResourceContext { get; }
44+
4045
public JsonApiResourceDefinition(IResourceGraph resourceGraph)
4146
{
4247
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
4348

4449
ResourceGraph = resourceGraph;
50+
ResourceContext = resourceGraph.GetResourceContext<TResource>();
4551
}
4652

4753
/// <inheritdoc />

src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JetBrains.Annotations;
55
using JsonApiDotNetCore.Configuration;
66
using JsonApiDotNetCore.Middleware;
7+
using JsonApiDotNetCore.Queries.Internal;
78
using JsonApiDotNetCore.Resources;
89
using JsonApiDotNetCore.Resources.Annotations;
910
using JsonApiDotNetCore.Serialization.Building;
@@ -21,27 +22,31 @@ public sealed class AtomicOperationsResponseSerializer : BaseSerializer, IJsonAp
2122
private readonly ILinkBuilder _linkBuilder;
2223
private readonly IFieldsToSerialize _fieldsToSerialize;
2324
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
25+
private readonly IEvaluatedIncludeCache _evaluatedIncludeCache;
2426
private readonly IJsonApiRequest _request;
2527
private readonly IJsonApiOptions _options;
2628

2729
/// <inheritdoc />
2830
public string ContentType { get; } = HeaderConstants.AtomicOperationsMediaType;
2931

3032
public AtomicOperationsResponseSerializer(IResourceObjectBuilder resourceObjectBuilder, IMetaBuilder metaBuilder, ILinkBuilder linkBuilder,
31-
IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request, IJsonApiOptions options)
33+
IFieldsToSerialize fieldsToSerialize, IResourceDefinitionAccessor resourceDefinitionAccessor, IEvaluatedIncludeCache evaluatedIncludeCache,
34+
IJsonApiRequest request, IJsonApiOptions options)
3235
: base(resourceObjectBuilder)
3336
{
3437
ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder));
3538
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
3639
ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize));
3740
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
41+
ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache));
3842
ArgumentGuard.NotNull(request, nameof(request));
3943
ArgumentGuard.NotNull(options, nameof(options));
4044

4145
_metaBuilder = metaBuilder;
4246
_linkBuilder = linkBuilder;
4347
_fieldsToSerialize = fieldsToSerialize;
4448
_resourceDefinitionAccessor = resourceDefinitionAccessor;
49+
_evaluatedIncludeCache = evaluatedIncludeCache;
4550
_request = request;
4651
_options = options;
4752
}
@@ -93,6 +98,7 @@ private AtomicResultObject SerializeOperation(OperationContainer operation)
9398
{
9499
_request.CopyFrom(operation.Request);
95100
_fieldsToSerialize.ResetCache();
101+
_evaluatedIncludeCache.Set(null);
96102

97103
_resourceDefinitionAccessor.OnSerialize(operation.Resource);
98104

src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using JsonApiDotNetCore.Queries;
66
using JsonApiDotNetCore.Queries.Expressions;
77
using JsonApiDotNetCore.Queries.Internal;
8-
using JsonApiDotNetCore.QueryStrings;
98
using JsonApiDotNetCore.Resources;
109
using JsonApiDotNetCore.Resources.Annotations;
1110
using JsonApiDotNetCore.Serialization.Objects;
@@ -17,28 +16,31 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder
1716
{
1817
private static readonly IncludeChainConverter IncludeChainConverter = new IncludeChainConverter();
1918

19+
private readonly ILinkBuilder _linkBuilder;
2020
private readonly IIncludedResourceObjectBuilder _includedBuilder;
21-
private readonly IEnumerable<IQueryConstraintProvider> _constraintProviders;
2221
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
23-
private readonly ILinkBuilder _linkBuilder;
22+
private readonly IEvaluatedIncludeCache _evaluatedIncludeCache;
2423
private readonly SparseFieldSetCache _sparseFieldSetCache;
24+
2525
private RelationshipAttribute _requestRelationship;
2626

2727
public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder,
2828
IEnumerable<IQueryConstraintProvider> constraintProviders, IResourceContextProvider resourceContextProvider,
29-
IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider)
29+
IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider,
30+
IEvaluatedIncludeCache evaluatedIncludeCache)
3031
: base(resourceContextProvider, settingsProvider.Get())
3132
{
3233
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
3334
ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder));
3435
ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders));
3536
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
37+
ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache));
3638

3739
_linkBuilder = linkBuilder;
3840
_includedBuilder = includedBuilder;
39-
_constraintProviders = constraintProviders;
4041
_resourceDefinitionAccessor = resourceDefinitionAccessor;
41-
_sparseFieldSetCache = new SparseFieldSetCache(_constraintProviders, resourceDefinitionAccessor);
42+
_evaluatedIncludeCache = evaluatedIncludeCache;
43+
_sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor);
4244
}
4345

4446
public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship)
@@ -72,7 +74,7 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r
7274
ArgumentGuard.NotNull(resource, nameof(resource));
7375

7476
RelationshipEntry relationshipEntry = null;
75-
IReadOnlyCollection<IReadOnlyCollection<RelationshipAttribute>> relationshipChains = GetInclusionChain(relationship);
77+
IReadOnlyCollection<IReadOnlyCollection<RelationshipAttribute>> relationshipChains = GetInclusionChainsStartingWith(relationship);
7678

7779
if (Equals(relationship, _requestRelationship) || relationshipChains.Any())
7880
{
@@ -116,35 +118,24 @@ private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship)
116118
}
117119

118120
/// <summary>
119-
/// Inspects the included relationship chains (see <see cref="IIncludeQueryStringParameterReader" /> to see if <paramref name="relationship" /> should be
120-
/// included or not.
121+
/// Inspects the included relationship chains and selects the ones that starts with the specified relationship.
121122
/// </summary>
122-
private IReadOnlyCollection<IReadOnlyCollection<RelationshipAttribute>> GetInclusionChain(RelationshipAttribute relationship)
123+
private IReadOnlyCollection<IReadOnlyCollection<RelationshipAttribute>> GetInclusionChainsStartingWith(RelationshipAttribute relationship)
123124
{
124-
// @formatter:wrap_chained_method_calls chop_always
125-
// @formatter:keep_existing_linebreaks true
126-
127-
ResourceFieldChainExpression[] chains = _constraintProviders
128-
.SelectMany(provider => provider.GetConstraints())
129-
.Select(expressionInScope => expressionInScope.Expression)
130-
.OfType<IncludeExpression>()
131-
.SelectMany(IncludeChainConverter.GetRelationshipChains)
132-
.ToArray();
133-
134-
// @formatter:keep_existing_linebreaks restore
135-
// @formatter:wrap_chained_method_calls restore
125+
IncludeExpression include = _evaluatedIncludeCache.Get() ?? IncludeExpression.Empty;
126+
IReadOnlyCollection<ResourceFieldChainExpression> chains = IncludeChainConverter.GetRelationshipChains(include);
136127

137-
var inclusionChain = new List<IReadOnlyCollection<RelationshipAttribute>>();
128+
var inclusionChains = new List<IReadOnlyCollection<RelationshipAttribute>>();
138129

139130
foreach (ResourceFieldChainExpression chain in chains)
140131
{
141132
if (chain.Fields.First().Equals(relationship))
142133
{
143-
inclusionChain.Add(chain.Fields.Cast<RelationshipAttribute>().ToArray());
134+
inclusionChains.Add(chain.Fields.Cast<RelationshipAttribute>().ToArray());
144135
}
145136
}
146137

147-
return inclusionChain;
138+
return inclusionChains;
148139
}
149140
}
150141
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ public sealed class TelevisionBroadcastDefinition : JsonApiResourceDefinition<Te
2424
private readonly TelevisionDbContext _dbContext;
2525
private readonly IJsonApiRequest _request;
2626
private readonly IEnumerable<IQueryConstraintProvider> _constraintProviders;
27-
private readonly ResourceContext _broadcastContext;
2827

2928
private DateTimeOffset? _storedArchivedAt;
3029

@@ -35,7 +34,6 @@ public TelevisionBroadcastDefinition(IResourceGraph resourceGraph, TelevisionDbC
3534
_dbContext = dbContext;
3635
_request = request;
3736
_constraintProviders = constraintProviders;
38-
_broadcastContext = resourceGraph.GetResourceContext<TelevisionBroadcast>();
3937
}
4038

4139
public override FilterExpression OnApplyFilter(FilterExpression existingFilter)
@@ -46,8 +44,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter)
4644

4745
if (IsReturningCollectionOfTelevisionBroadcasts() && !HasFilterOnArchivedAt(existingFilter))
4846
{
49-
AttrAttribute archivedAtAttribute =
50-
_broadcastContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt));
47+
AttrAttribute archivedAtAttribute = ResourceContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt));
5148

5249
var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute);
5350

@@ -71,7 +68,7 @@ private bool IsRequestingCollectionOfTelevisionBroadcasts()
7168
{
7269
if (_request.IsCollection)
7370
{
74-
if (_request.PrimaryResource == _broadcastContext || _request.SecondaryResource == _broadcastContext)
71+
if (_request.PrimaryResource == ResourceContext || _request.SecondaryResource == ResourceContext)
7572
{
7673
return true;
7774
}
@@ -97,7 +94,7 @@ private bool IsIncludingCollectionOfTelevisionBroadcasts()
9794

9895
foreach (IncludeElementExpression includeElement in includeElements)
9996
{
100-
if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType == _broadcastContext.ResourceType)
97+
if (includeElement.Relationship is HasManyAttribute && includeElement.Relationship.RightType == ResourceContext.ResourceType)
10198
{
10299
return true;
103100
}

0 commit comments

Comments
 (0)