diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index b0bb81f44b..170eb2d97c 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -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; @@ -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 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(ResourceIdentifierObjectComparer.Instance); _fieldsToSerialize = fieldsToSerialize; _linkBuilder = linkBuilder; _resourceDefinitionAccessor = resourceDefinitionAccessor; + _queryStringAccessor = queryStringAccessor; _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } @@ -43,7 +47,7 @@ public IList 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) @@ -57,7 +61,7 @@ public IList Build() return _included.ToArray(); } - return null; + return _queryStringAccessor.Query.ContainsKey("include") ? Array.Empty() : null; } private void UpdateRelationships(ResourceObject resourceObject) diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs index 4f6b6eb3cf..ec8198646d 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTests/Serialization/SerializationTests.cs @@ -160,6 +160,63 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }"); } + [Fact] + public async Task Can_get_primary_resources_with_empty_include() + { + // Arrange + List meetings = _fakers.Meeting.Generate(1); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.Meetings.AddRange(meetings); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/meetings/?include=attendees"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(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() { diff --git a/test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs b/test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs new file mode 100644 index 0000000000..a2cfc2bebb --- /dev/null +++ b/test/UnitTests/Serialization/FakeRequestQueryStringAccessor.cs @@ -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)); + } + } +} diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 24a833bac5..31b704a4dd 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -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; @@ -56,7 +57,7 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); - IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(); + IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChainArray != null); IFieldsToSerialize fieldsToSerialize = GetSerializableFields(); IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); IResourceObjectBuilderSettingsProvider settingsProvider = GetSerializerSettingsProvider(); @@ -77,17 +78,23 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumer ILinkBuilder link = GetLinkBuilder(null, resourceLinks, relationshipLinks); IEnumerable 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(), - 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(), + resourceDefinitionAccessor, queryStringAccessor, resourceObjectBuilderSettingsProvider); } protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider() diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index 8270c6edb2..ff9a2d5855 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -180,11 +180,12 @@ private IncludedResourceObjectBuilder GetBuilder() { IFieldsToSerialize fields = GetSerializableFields(); ILinkBuilder links = GetLinkBuilder(); + IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; + var queryStringAccessor = new FakeRequestQueryStringAccessor(); + IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider(); - IResourceDefinitionAccessor accessor = new Mock().Object; - - return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty(), accessor, - GetSerializerSettingsProvider()); + return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty(), resourceDefinitionAccessor, + queryStringAccessor, resourceObjectBuilderSettingsProvider); } private sealed class AuthorChainInstances