Skip to content

Commit 886c5ad

Browse files
author
Bart Koelman
authored
Always emit included array in response when query string contains include parameter (#1012)
1 parent 67e0739 commit 886c5ad

File tree

5 files changed

+102
-12
lines changed

5 files changed

+102
-12
lines changed

src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs

+7-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Configuration;
77
using JsonApiDotNetCore.Queries;
88
using JsonApiDotNetCore.Queries.Internal;
9+
using JsonApiDotNetCore.QueryStrings;
910
using JsonApiDotNetCore.Resources;
1011
using JsonApiDotNetCore.Resources.Annotations;
1112
using JsonApiDotNetCore.Serialization.Objects;
@@ -19,22 +20,25 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes
1920
private readonly IFieldsToSerialize _fieldsToSerialize;
2021
private readonly ILinkBuilder _linkBuilder;
2122
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
23+
private readonly IRequestQueryStringAccessor _queryStringAccessor;
2224
private readonly SparseFieldSetCache _sparseFieldSetCache;
2325

2426
public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceContextProvider resourceContextProvider,
2527
IEnumerable<IQueryConstraintProvider> constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor,
26-
IResourceObjectBuilderSettingsProvider settingsProvider)
28+
IRequestQueryStringAccessor queryStringAccessor, IResourceObjectBuilderSettingsProvider settingsProvider)
2729
: base(resourceContextProvider, settingsProvider.Get())
2830
{
2931
ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize));
3032
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
3133
ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders));
3234
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
35+
ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor));
3336

3437
_included = new HashSet<ResourceObject>(ResourceIdentifierObjectComparer.Instance);
3538
_fieldsToSerialize = fieldsToSerialize;
3639
_linkBuilder = linkBuilder;
3740
_resourceDefinitionAccessor = resourceDefinitionAccessor;
41+
_queryStringAccessor = queryStringAccessor;
3842
_sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor);
3943
}
4044

@@ -43,7 +47,7 @@ public IList<ResourceObject> Build()
4347
{
4448
if (_included.Any())
4549
{
46-
// cleans relationship dictionaries and adds links of resources.
50+
// Cleans relationship dictionaries and adds links of resources.
4751
foreach (ResourceObject resourceObject in _included)
4852
{
4953
if (resourceObject.Relationships != null)
@@ -57,7 +61,7 @@ public IList<ResourceObject> Build()
5761
return _included.ToArray();
5862
}
5963

60-
return null;
64+
return _queryStringAccessor.Query.ContainsKey("include") ? Array.Empty<ResourceObject>() : null;
6165
}
6266

6367
private void UpdateRelationships(ResourceObject resourceObject)

test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs

+57
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,63 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
160160
}");
161161
}
162162

163+
[Fact]
164+
public async Task Can_get_primary_resources_with_empty_include()
165+
{
166+
// Arrange
167+
List<Meeting> meetings = _fakers.Meeting.Generate(1);
168+
169+
await _testContext.RunOnDatabaseAsync(async dbContext =>
170+
{
171+
await dbContext.ClearTableAsync<Meeting>();
172+
dbContext.Meetings.AddRange(meetings);
173+
await dbContext.SaveChangesAsync();
174+
});
175+
176+
const string route = "/meetings/?include=attendees";
177+
178+
// Act
179+
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync<string>(route);
180+
181+
// Assert
182+
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);
183+
184+
responseDocument.Should().BeJson(@"{
185+
""links"": {
186+
""self"": ""http://localhost/meetings/?include=attendees"",
187+
""first"": ""http://localhost/meetings/?include=attendees""
188+
},
189+
""data"": [
190+
{
191+
""type"": ""meetings"",
192+
""id"": """ + meetings[0].StringId + @""",
193+
""attributes"": {
194+
""title"": """ + meetings[0].Title + @""",
195+
""startTime"": """ + meetings[0].StartTime.ToString("O") + @""",
196+
""duration"": """ + meetings[0].Duration + @""",
197+
""location"": {
198+
""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @",
199+
""lng"": " + meetings[0].Location.Longitude.ToString(CultureInfo.InvariantCulture) + @"
200+
}
201+
},
202+
""relationships"": {
203+
""attendees"": {
204+
""links"": {
205+
""self"": ""http://localhost/meetings/" + meetings[0].StringId + @"/relationships/attendees"",
206+
""related"": ""http://localhost/meetings/" + meetings[0].StringId + @"/attendees""
207+
},
208+
""data"": []
209+
}
210+
},
211+
""links"": {
212+
""self"": ""http://localhost/meetings/" + meetings[0].StringId + @"""
213+
}
214+
}
215+
],
216+
""included"": []
217+
}");
218+
}
219+
163220
[Fact]
164221
public async Task Can_get_primary_resource_by_ID()
165222
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using JsonApiDotNetCore.QueryStrings;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.WebUtilities;
4+
5+
namespace UnitTests.Serialization
6+
{
7+
internal sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor
8+
{
9+
public IQueryCollection Query { get; }
10+
11+
public FakeRequestQueryStringAccessor()
12+
: this(null)
13+
{
14+
}
15+
16+
public FakeRequestQueryStringAccessor(string queryString)
17+
{
18+
Query = string.IsNullOrEmpty(queryString) ? QueryCollection.Empty : new QueryCollection(QueryHelpers.ParseQuery(queryString));
19+
}
20+
}
21+
}

test/UnitTests/Serialization/SerializerTestsSetup.cs

+12-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Queries;
77
using JsonApiDotNetCore.Queries.Expressions;
88
using JsonApiDotNetCore.Queries.Internal;
9+
using JsonApiDotNetCore.QueryStrings;
910
using JsonApiDotNetCore.Resources;
1011
using JsonApiDotNetCore.Resources.Annotations;
1112
using JsonApiDotNetCore.Serialization;
@@ -56,7 +57,7 @@ protected ResponseSerializer<T> GetResponseSerializer<T>(IEnumerable<IEnumerable
5657
IMetaBuilder meta = GetMetaBuilder(metaDict);
5758
ILinkBuilder link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks);
5859
IEnumerable<IQueryConstraintProvider> includeConstraints = GetIncludeConstraints(inclusionChainArray);
59-
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder();
60+
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChainArray != null);
6061
IFieldsToSerialize fieldsToSerialize = GetSerializableFields();
6162
IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor();
6263
IResourceObjectBuilderSettingsProvider settingsProvider = GetSerializerSettingsProvider();
@@ -77,17 +78,23 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumer
7778

7879
ILinkBuilder link = GetLinkBuilder(null, resourceLinks, relationshipLinks);
7980
IEnumerable<IQueryConstraintProvider> includeConstraints = GetIncludeConstraints(inclusionChainArray);
80-
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder();
81+
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChains != null);
8182
IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray);
8283

8384
return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(),
8485
GetSerializerSettingsProvider(), evaluatedIncludeCache);
8586
}
8687

87-
private IIncludedResourceObjectBuilder GetIncludedBuilder()
88+
private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQueryString)
8889
{
89-
return new IncludedResourceObjectBuilder(GetSerializableFields(), GetLinkBuilder(), ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(),
90-
GetResourceDefinitionAccessor(), GetSerializerSettingsProvider());
90+
IFieldsToSerialize fieldsToSerialize = GetSerializableFields();
91+
ILinkBuilder linkBuilder = GetLinkBuilder();
92+
IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor();
93+
IRequestQueryStringAccessor queryStringAccessor = new FakeRequestQueryStringAccessor(hasIncludeQueryString ? "include=" : null);
94+
IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider();
95+
96+
return new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(),
97+
resourceDefinitionAccessor, queryStringAccessor, resourceObjectBuilderSettingsProvider);
9198
}
9299

93100
protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider()

test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,12 @@ private IncludedResourceObjectBuilder GetBuilder()
180180
{
181181
IFieldsToSerialize fields = GetSerializableFields();
182182
ILinkBuilder links = GetLinkBuilder();
183+
IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock<IResourceDefinitionAccessor>().Object;
184+
var queryStringAccessor = new FakeRequestQueryStringAccessor();
185+
IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider();
183186

184-
IResourceDefinitionAccessor accessor = new Mock<IResourceDefinitionAccessor>().Object;
185-
186-
return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(), accessor,
187-
GetSerializerSettingsProvider());
187+
return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(), resourceDefinitionAccessor,
188+
queryStringAccessor, resourceObjectBuilderSettingsProvider);
188189
}
189190

190191
private sealed class AuthorChainInstances

0 commit comments

Comments
 (0)