Skip to content

Commit 883f65b

Browse files
author
Bart Koelman
committed
Makes the serializer take changes from IResourceDefinition.OnApplyChanges into account. This enables adding extra includes from a resource definition.
1 parent 756dab9 commit 883f65b

File tree

12 files changed

+177
-34
lines changed

12 files changed

+177
-34
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: 6 additions & 1 deletion
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

src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs

Lines changed: 6 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,30 @@ 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, IJsonApiRequest request, IJsonApiOptions options)
3234
: base(resourceObjectBuilder)
3335
{
3436
ArgumentGuard.NotNull(metaBuilder, nameof(metaBuilder));
3537
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
3638
ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize));
3739
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
40+
ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache));
3841
ArgumentGuard.NotNull(request, nameof(request));
3942
ArgumentGuard.NotNull(options, nameof(options));
4043

4144
_metaBuilder = metaBuilder;
4245
_linkBuilder = linkBuilder;
4346
_fieldsToSerialize = fieldsToSerialize;
4447
_resourceDefinitionAccessor = resourceDefinitionAccessor;
48+
_evaluatedIncludeCache = evaluatedIncludeCache;
4549
_request = request;
4650
_options = options;
4751
}
@@ -93,6 +97,7 @@ private AtomicResultObject SerializeOperation(OperationContainer operation)
9397
{
9498
_request.CopyFrom(operation.Request);
9599
_fieldsToSerialize.ResetCache();
100+
_evaluatedIncludeCache.Set(null);
96101

97102
_resourceDefinitionAccessor.OnSerialize(operation.Resource);
98103

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/ResourceDefinitions/Reading/IClientSettingsProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ public interface IClientSettingsProvider
44
{
55
bool IsIncludePlanetMoonsBlocked { get; }
66
bool ArePlanetsWithPrivateNameHidden { get; }
7+
bool IsMoonOrbitingPlanetAutoIncluded { get; }
78
}
89
}

test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,45 @@
1+
using System.Collections.Generic;
12
using System.Linq;
23
using JetBrains.Annotations;
34
using JsonApiDotNetCore.Configuration;
5+
using JsonApiDotNetCore.Queries.Expressions;
46
using JsonApiDotNetCore.Resources;
7+
using JsonApiDotNetCore.Resources.Annotations;
58
using Microsoft.Extensions.Primitives;
69

710
namespace JsonApiDotNetCoreExampleTests.IntegrationTests.ResourceDefinitions.Reading
811
{
912
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
1013
public sealed class MoonDefinition : JsonApiResourceDefinition<Moon>
1114
{
12-
public MoonDefinition(IResourceGraph resourceGraph)
15+
private readonly IClientSettingsProvider _clientSettingsProvider;
16+
17+
public MoonDefinition(IResourceGraph resourceGraph, IClientSettingsProvider clientSettingsProvider)
1318
: base(resourceGraph)
1419
{
1520
// This constructor will be resolved from the container, which means
1621
// you can take on any dependency that is also defined in the container.
22+
23+
_clientSettingsProvider = clientSettingsProvider;
24+
}
25+
26+
public override IReadOnlyCollection<IncludeElementExpression> OnApplyIncludes(IReadOnlyCollection<IncludeElementExpression> existingIncludes)
27+
{
28+
if (!_clientSettingsProvider.IsMoonOrbitingPlanetAutoIncluded ||
29+
existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Moon.OrbitsAround)))
30+
{
31+
return existingIncludes;
32+
}
33+
34+
ResourceContext resourceContext = ResourceGraph.GetResourceContext<Moon>();
35+
36+
RelationshipAttribute orbitsAroundRelationship =
37+
resourceContext.Relationships.Single(relationship => relationship.Property.Name == nameof(Moon.OrbitsAround));
38+
39+
return new List<IncludeElementExpression>(existingIncludes)
40+
{
41+
new IncludeElementExpression(orbitsAroundRelationship)
42+
};
1743
}
1844

1945
public override QueryStringParameterHandlers<Moon> OnRegisterQueryableHandlersForQueryStringParameters()

test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,40 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
7474
error.Detail.Should().BeNull();
7575
}
7676

77+
[Fact]
78+
public async Task Include_from_resource_definition_is_added()
79+
{
80+
// Arrange
81+
var settingsProvider = (TestClientSettingsProvider)_testContext.Factory.Services.GetRequiredService<IClientSettingsProvider>();
82+
settingsProvider.AutoIncludeOrbitingPlanetForMoons();
83+
84+
Moon moon = _fakers.Moon.Generate();
85+
moon.OrbitsAround = _fakers.Planet.Generate();
86+
87+
await _testContext.RunOnDatabaseAsync(async dbContext =>
88+
{
89+
dbContext.Moons.Add(moon);
90+
await dbContext.SaveChangesAsync();
91+
});
92+
93+
string route = "/moons/" + moon.StringId;
94+
95+
// Act
96+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync<Document>(route);
97+
98+
// Assert
99+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
100+
101+
responseDocument.SingleData.Should().NotBeNull();
102+
responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Type.Should().Be("planets");
103+
responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Id.Should().Be(moon.OrbitsAround.StringId);
104+
105+
responseDocument.Included.Should().HaveCount(1);
106+
responseDocument.Included[0].Type.Should().Be("planets");
107+
responseDocument.Included[0].Id.Should().Be(moon.OrbitsAround.StringId);
108+
responseDocument.Included[0].Attributes["publicName"].Should().Be(moon.OrbitsAround.PublicName);
109+
}
110+
77111
[Fact]
78112
public async Task Filter_from_resource_definition_is_applied()
79113
{

test/JsonApiDotNetCoreExampleTests/IntegrationTests/ResourceDefinitions/Reading/TestClientSettingsProvider.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ internal sealed class TestClientSettingsProvider : IClientSettingsProvider
44
{
55
public bool IsIncludePlanetMoonsBlocked { get; private set; }
66
public bool ArePlanetsWithPrivateNameHidden { get; private set; }
7+
public bool IsMoonOrbitingPlanetAutoIncluded { get; private set; }
78

89
public void ResetToDefaults()
910
{
1011
IsIncludePlanetMoonsBlocked = false;
1112
ArePlanetsWithPrivateNameHidden = false;
13+
IsMoonOrbitingPlanetAutoIncluded = false;
1214
}
1315

1416
public void BlockIncludePlanetMoons()
@@ -20,5 +22,10 @@ public void HidePlanetsWithPrivateName()
2022
{
2123
ArePlanetsWithPrivateNameHidden = true;
2224
}
25+
26+
public void AutoIncludeOrbitingPlanetForMoons()
27+
{
28+
IsMoonOrbitingPlanetAutoIncluded = true;
29+
}
2330
}
2431
}

0 commit comments

Comments
 (0)