Skip to content

Always emit included array in response when query string contains include parameter #1012

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
Expand All @@ -19,22 +20,25 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes
private readonly IFieldsToSerialize _fieldsToSerialize;
private readonly ILinkBuilder _linkBuilder;
private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor;
private readonly IRequestQueryStringAccessor _queryStringAccessor;
private readonly SparseFieldSetCache _sparseFieldSetCache;

public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceContextProvider resourceContextProvider,
IEnumerable<IQueryConstraintProvider> constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor,
IResourceObjectBuilderSettingsProvider settingsProvider)
IRequestQueryStringAccessor queryStringAccessor, IResourceObjectBuilderSettingsProvider settingsProvider)
: base(resourceContextProvider, settingsProvider.Get())
{
ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize));
ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder));
ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders));
ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor));
ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor));

_included = new HashSet<ResourceObject>(ResourceIdentifierObjectComparer.Instance);
_fieldsToSerialize = fieldsToSerialize;
_linkBuilder = linkBuilder;
_resourceDefinitionAccessor = resourceDefinitionAccessor;
_queryStringAccessor = queryStringAccessor;
_sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor);
}

Expand All @@ -43,7 +47,7 @@ public IList<ResourceObject> Build()
{
if (_included.Any())
{
// cleans relationship dictionaries and adds links of resources.
// Cleans relationship dictionaries and adds links of resources.
foreach (ResourceObject resourceObject in _included)
{
if (resourceObject.Relationships != null)
Expand All @@ -57,7 +61,7 @@ public IList<ResourceObject> Build()
return _included.ToArray();
}

return null;
return _queryStringAccessor.Query.ContainsKey("include") ? Array.Empty<ResourceObject>() : null;
}

private void UpdateRelationships(ResourceObject resourceObject)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,63 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
}");
}

[Fact]
public async Task Can_get_primary_resources_with_empty_include()
{
// Arrange
List<Meeting> meetings = _fakers.Meeting.Generate(1);

await _testContext.RunOnDatabaseAsync(async dbContext =>
{
await dbContext.ClearTableAsync<Meeting>();
dbContext.Meetings.AddRange(meetings);
await dbContext.SaveChangesAsync();
});

const string route = "/meetings/?include=attendees";

// Act
(HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync<string>(route);

// Assert
httpResponse.Should().HaveStatusCode(HttpStatusCode.OK);

responseDocument.Should().BeJson(@"{
""links"": {
""self"": ""http://localhost/meetings/?include=attendees"",
""first"": ""http://localhost/meetings/?include=attendees""
},
""data"": [
{
""type"": ""meetings"",
""id"": """ + meetings[0].StringId + @""",
""attributes"": {
""title"": """ + meetings[0].Title + @""",
""startTime"": """ + meetings[0].StartTime.ToString("O") + @""",
""duration"": """ + meetings[0].Duration + @""",
""location"": {
""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @",
""lng"": " + meetings[0].Location.Longitude.ToString(CultureInfo.InvariantCulture) + @"
}
},
""relationships"": {
""attendees"": {
""links"": {
""self"": ""http://localhost/meetings/" + meetings[0].StringId + @"/relationships/attendees"",
""related"": ""http://localhost/meetings/" + meetings[0].StringId + @"/attendees""
},
""data"": []
}
},
""links"": {
""self"": ""http://localhost/meetings/" + meetings[0].StringId + @"""
}
}
],
""included"": []
}");
}

[Fact]
public async Task Can_get_primary_resource_by_ID()
{
Expand Down
21 changes: 21 additions & 0 deletions test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using JsonApiDotNetCore.QueryStrings;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;

namespace UnitTests.Serialization
{
internal sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor
{
public IQueryCollection Query { get; }

public FakeRequestQueryStringAccessor()
: this(null)
{
}

public FakeRequestQueryStringAccessor(string queryString)
{
Query = string.IsNullOrEmpty(queryString) ? QueryCollection.Empty : new QueryCollection(QueryHelpers.ParseQuery(queryString));
}
}
}
17 changes: 12 additions & 5 deletions test/UnitTests/Serialization/SerializerTestsSetup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization;
Expand Down Expand Up @@ -56,7 +57,7 @@ protected ResponseSerializer<T> GetResponseSerializer<T>(IEnumerable<IEnumerable
IMetaBuilder meta = GetMetaBuilder(metaDict);
ILinkBuilder link = GetLinkBuilder(topLinks, resourceLinks, relationshipLinks);
IEnumerable<IQueryConstraintProvider> includeConstraints = GetIncludeConstraints(inclusionChainArray);
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder();
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChainArray != null);
IFieldsToSerialize fieldsToSerialize = GetSerializableFields();
IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor();
IResourceObjectBuilderSettingsProvider settingsProvider = GetSerializerSettingsProvider();
Expand All @@ -77,17 +78,23 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumer

ILinkBuilder link = GetLinkBuilder(null, resourceLinks, relationshipLinks);
IEnumerable<IQueryConstraintProvider> includeConstraints = GetIncludeConstraints(inclusionChainArray);
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder();
IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChains != null);
IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray);

return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(),
GetSerializerSettingsProvider(), evaluatedIncludeCache);
}

private IIncludedResourceObjectBuilder GetIncludedBuilder()
private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQueryString)
{
return new IncludedResourceObjectBuilder(GetSerializableFields(), GetLinkBuilder(), ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(),
GetResourceDefinitionAccessor(), GetSerializerSettingsProvider());
IFieldsToSerialize fieldsToSerialize = GetSerializableFields();
ILinkBuilder linkBuilder = GetLinkBuilder();
IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor();
IRequestQueryStringAccessor queryStringAccessor = new FakeRequestQueryStringAccessor(hasIncludeQueryString ? "include=" : null);
IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider();

return new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(),
resourceDefinitionAccessor, queryStringAccessor, resourceObjectBuilderSettingsProvider);
}

protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,12 @@ private IncludedResourceObjectBuilder GetBuilder()
{
IFieldsToSerialize fields = GetSerializableFields();
ILinkBuilder links = GetLinkBuilder();
IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock<IResourceDefinitionAccessor>().Object;
var queryStringAccessor = new FakeRequestQueryStringAccessor();
IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider();

IResourceDefinitionAccessor accessor = new Mock<IResourceDefinitionAccessor>().Object;

return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(), accessor,
GetSerializerSettingsProvider());
return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty<IQueryConstraintProvider>(), resourceDefinitionAccessor,
queryStringAccessor, resourceObjectBuilderSettingsProvider);
}

private sealed class AuthorChainInstances
Expand Down