diff --git a/JsonApiDotnetCore.sln b/JsonApiDotnetCore.sln index 385fa4d6ad..0425e09f96 100644 --- a/JsonApiDotnetCore.sln +++ b/JsonApiDotnetCore.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.10 +VisualStudioVersion = 15.0.27004.2009 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore", "src\JsonApiDotNetCore\JsonApiDotNetCore.csproj", "{C0EC9E70-EB2E-436F-9D94-FA16FA774123}" EndProject @@ -30,7 +30,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReportsExample", "src\Examp EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{076E1AE4-FD25-4684-B826-CAAE37FEA0AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{1F604666-BB0F-413E-922D-9D37C6073285}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "benchmarks\Benchmarks.csproj", "{1F604666-BB0F-413E-922D-9D37C6073285}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -110,22 +110,22 @@ Global {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Debug|x86.Build.0 = Debug|Any CPU {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|Any CPU.ActiveCfg = Release|Any CPU {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|Any CPU.Build.0 = Release|Any CPU - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.ActiveCfg = Release|x64 - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.Build.0 = Release|x64 - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.ActiveCfg = Release|x86 - {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.Build.0 = Release|x86 + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.ActiveCfg = Release|Any CPU + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x64.Build.0 = Release|Any CPU + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.ActiveCfg = Release|Any CPU + {FBFB0B0B-EA86-4B41-AB2A-E0249F70C86D}.Release|x86.Build.0 = Release|Any CPU {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.ActiveCfg = Debug|x64 - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.Build.0 = Debug|x64 - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.ActiveCfg = Debug|x86 - {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.Build.0 = Debug|x86 + {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x64.Build.0 = Debug|Any CPU + {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F604666-BB0F-413E-922D-9D37C6073285}.Debug|x86.Build.0 = Debug|Any CPU {1F604666-BB0F-413E-922D-9D37C6073285}.Release|Any CPU.ActiveCfg = Release|Any CPU {1F604666-BB0F-413E-922D-9D37C6073285}.Release|Any CPU.Build.0 = Release|Any CPU - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.ActiveCfg = Release|x64 - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.Build.0 = Release|x64 - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.ActiveCfg = Release|x86 - {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.Build.0 = Release|x86 + {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.ActiveCfg = Release|Any CPU + {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x64.Build.0 = Release|Any CPU + {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.ActiveCfg = Release|Any CPU + {1F604666-BB0F-413E-922D-9D37C6073285}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs index ac7e1b3ade..9e2517305e 100644 --- a/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilder.cs @@ -13,18 +13,14 @@ public class DocumentBuilder : IDocumentBuilder private readonly IJsonApiContext _jsonApiContext; private readonly IContextGraph _contextGraph; private readonly IRequestMeta _requestMeta; + private readonly DocumentBuilderOptions _documentBuilderOptions; - public DocumentBuilder(IJsonApiContext jsonApiContext) - { - _jsonApiContext = jsonApiContext; - _contextGraph = jsonApiContext.ContextGraph; - } - - public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta) + public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta=null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider=null) { _jsonApiContext = jsonApiContext; _contextGraph = jsonApiContext.ContextGraph; _requestMeta = requestMeta; + _documentBuilderOptions = documentBuilderOptionsProvider?.GetDocumentBuilderOptions() ?? new DocumentBuilderOptions(); ; } public Document Build(IIdentifiable entity) @@ -118,8 +114,11 @@ private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) contextEntity.Attributes.ForEach(attr => { - if(ShouldIncludeAttribute(attr)) - data.Attributes.Add(attr.PublicAttributeName, attr.GetValue(entity)); + var attributeValue = attr.GetValue(entity); + if (ShouldIncludeAttribute(attr, attributeValue)) + { + data.Attributes.Add(attr.PublicAttributeName, attributeValue); + } }); if (contextEntity.Relationships.Count > 0) @@ -128,11 +127,17 @@ private DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity) return data; } - private bool ShouldIncludeAttribute(AttrAttribute attr) + private bool ShouldIncludeAttribute(AttrAttribute attr, object attributeValue) + { + return !OmitNullValuedAttribute(attr, attributeValue) + && ((_jsonApiContext.QuerySet == null + || _jsonApiContext.QuerySet.Fields.Count == 0) + || _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName)); + } + + private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue) { - return (_jsonApiContext.QuerySet == null - || _jsonApiContext.QuerySet.Fields.Count == 0 - || _jsonApiContext.QuerySet.Fields.Contains(attr.InternalAttributeName)); + return attributeValue == null && _documentBuilderOptions.OmitNullValuedAttributes; } private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity) diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs new file mode 100644 index 0000000000..ec19977313 --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Builders +{ + public struct DocumentBuilderOptions + { + public DocumentBuilderOptions(bool omitNullValuedAttributes = false) + { + this.OmitNullValuedAttributes = omitNullValuedAttributes; + } + + public bool OmitNullValuedAttributes { get; private set; } + } +} diff --git a/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs new file mode 100644 index 0000000000..af7fb78d7c --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Builders +{ + public class DocumentBuilderOptionsProvider : IDocumentBuilderOptionsProvider + { + private readonly IJsonApiContext _jsonApiContext; + private readonly IHttpContextAccessor _httpContextAccessor; + + public DocumentBuilderOptionsProvider(IJsonApiContext jsonApiContext, IHttpContextAccessor httpContextAccessor) + { + _jsonApiContext = jsonApiContext; + _httpContextAccessor = httpContextAccessor; + } + + public DocumentBuilderOptions GetDocumentBuilderOptions() + { + var nullAttributeResponseBehaviorConfig = this._jsonApiContext.Options.NullAttributeResponseBehavior; + if (nullAttributeResponseBehaviorConfig.AllowClientOverride && _httpContextAccessor.HttpContext.Request.Query.TryGetValue("omitNullValuedAttributes", out var omitNullValuedAttributesQs)) + { + if (bool.TryParse(omitNullValuedAttributesQs, out var omitNullValuedAttributes)) + { + return new DocumentBuilderOptions(omitNullValuedAttributes); + } + } + return new DocumentBuilderOptions(this._jsonApiContext.Options.NullAttributeResponseBehavior.OmitNullValuedAttributes); + } + } +} diff --git a/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs b/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs new file mode 100644 index 0000000000..d8effd4fe3 --- /dev/null +++ b/src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Builders +{ + public interface IDocumentBuilderOptionsProvider + { + DocumentBuilderOptions GetDocumentBuilderOptions(); + } +} diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 26e16b0741..f074940d9c 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -17,6 +17,7 @@ public class JsonApiOptions public IContextGraph ContextGraph { get; set; } public bool RelativeLinks { get; set; } public bool AllowCustomQueryParameters { get; set; } + public NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } [Obsolete("JsonContract resolver can now be set on SerializerSettings.")] public IContractResolver JsonContractResolver @@ -29,6 +30,7 @@ public IContractResolver JsonContractResolver NullValueHandling = NullValueHandling.Ignore, ContractResolver = new DasherizedResolver() }; + internal IContextGraphBuilder ContextGraphBuilder { get; } = new ContextGraphBuilder(); public void BuildContextGraph(Action builder) where TContext : DbContext @@ -49,4 +51,6 @@ public void BuildContextGraph(Action builder) ContextGraph = ContextGraphBuilder.Build(); } } + + } diff --git a/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs b/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs new file mode 100644 index 0000000000..1b10140f5e --- /dev/null +++ b/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JsonApiDotNetCore.Configuration +{ + public struct NullAttributeResponseBehavior + { + public NullAttributeResponseBehavior(bool omitNullValuedAttributes = false, bool allowClientOverride = false) + { + OmitNullValuedAttributes = omitNullValuedAttributes; + AllowClientOverride = allowClientOverride; + } + + public bool OmitNullValuedAttributes { get; } + public bool AllowClientOverride { get; } + // ... + } +} diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index 1549e77e0f..d75ce26c59 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -112,6 +112,7 @@ public static void AddJsonApiInternals( services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions) diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs index 1c0c5014f7..6fa02c90c4 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs @@ -29,6 +29,7 @@ public object Deserialize(string requestBody) try { var document = JsonConvert.DeserializeObject(requestBody); + _jsonApiContext.DocumentMeta = document.Meta; var entity = DocumentToObject(document.Data); return entity; } @@ -222,4 +223,4 @@ private object SetHasManyRelationship(object entity, return entity; } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs index ee7ee10a35..c16da81cfa 100644 --- a/src/JsonApiDotNetCore/Services/IJsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/IJsonApiContext.cs @@ -26,6 +26,8 @@ public interface IJsonApiContext Dictionary AttributesToUpdate { get; set; } Dictionary RelationshipsToUpdate { get; set; } Type ControllerType { get; set; } + Dictionary DocumentMeta { get; set; } + TAttribute GetControllerAttribute() where TAttribute : Attribute; } } diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs index 5304d77b29..ef6b4159ee 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs @@ -50,6 +50,7 @@ public JsonApiContext( public Dictionary AttributesToUpdate { get; set; } = new Dictionary(); public Dictionary RelationshipsToUpdate { get; set; } = new Dictionary(); public Type ControllerType { get; set; } + public Dictionary DocumentMeta { get; set; } public IJsonApiContext ApplyContext(object controller) { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs new file mode 100644 index 0000000000..250ab80d30 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + [Collection("WebHostCollection")] + public class NullValuedAttributeHandlingTests : IAsyncLifetime + { + private readonly TestFixture _fixture; + private readonly AppDbContext _dbContext; + private readonly TodoItem _todoItem; + + public NullValuedAttributeHandlingTests(TestFixture fixture) + { + _fixture = fixture; + _dbContext = fixture.GetService(); + _todoItem = new TodoItem + { + Description = null, + Ordinal = 1, + CreatedDate = DateTime.Now, + AchievedDate = DateTime.Now.AddDays(2) + }; + _todoItem = _dbContext.TodoItems.Add(_todoItem).Entity; + } + + public async Task InitializeAsync() + { + await _dbContext.SaveChangesAsync(); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + + [Theory] + [InlineData(null, null, null, false)] + [InlineData(true, null, null, true)] + [InlineData(false, true, "true", true)] + [InlineData(false, false, "true", false)] + [InlineData(true, true, "false", false)] + [InlineData(true, false, "false", true)] + [InlineData(null, false, "false", false)] + [InlineData(null, false, "true", false)] + [InlineData(null, true, "true", true)] + [InlineData(null, true, "false", false)] + [InlineData(null, true, "foo", false)] + [InlineData(null, false, "foo", false)] + [InlineData(true, true, "foo", true)] + [InlineData(true, false, "foo", true)] + [InlineData(null, true, null, false)] + [InlineData(null, false, null, false)] + public async Task CheckNullBehaviorCombination(bool? omitNullValuedAttributes, bool? allowClientOverride, + string clientOverride, bool omitsNulls) + { + + // Override some null handling options + NullAttributeResponseBehavior nullAttributeResponseBehavior; + if (omitNullValuedAttributes.HasValue && allowClientOverride.HasValue) + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value, allowClientOverride.Value); + } + else if (omitNullValuedAttributes.HasValue) + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value); + } + else if (allowClientOverride.HasValue) + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(allowClientOverride: allowClientOverride.Value); + } + else + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); + } + var jsonApiOptions = _fixture.GetService(); + jsonApiOptions.NullAttributeResponseBehavior = nullAttributeResponseBehavior; + jsonApiOptions.AllowCustomQueryParameters = true; + + var httpMethod = new HttpMethod("GET"); + var queryString = allowClientOverride.HasValue + ? $"?omitNullValuedAttributes={clientOverride}" + : ""; + var route = $"/api/v1/todo-items/{_todoItem.Id}{queryString}"; + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializeBody = JsonConvert.DeserializeObject(body); + + // assert. does response contain a null valued attribute + Assert.Equal(omitsNulls, !deserializeBody.Data.Attributes.ContainsKey("description")); + + } + } + +} diff --git a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs index 5d68306493..b654727a26 100644 --- a/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Unit/Extensions/IServiceCollectionExtensionsTests.cs @@ -49,6 +49,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services() Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService()); + Assert.NotNull(provider.GetService()); Assert.NotNull(provider.GetService(typeof(GenericProcessor))); } } diff --git a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs new file mode 100644 index 0000000000..333950f95f --- /dev/null +++ b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Text; +using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Http; +using Moq; +using Xunit; + +namespace UnitTests.Builders +{ + public class DocumentBuilderBehaviour_Tests + { + + [Theory] + [InlineData(null, null, null, false)] + [InlineData(false, null, null, false)] + [InlineData(true, null, null, true)] + [InlineData(false, false, "true", false)] + [InlineData(false, true, "true", true)] + [InlineData(true, true, "false", false)] + [InlineData(true, false, "false", true)] + [InlineData(null, false, "false", false)] + [InlineData(null, false, "true", false)] + [InlineData(null, true, "true", true)] + [InlineData(null, true, "false", false)] + [InlineData(null, true, "foo", false)] + [InlineData(null, false, "foo", false)] + [InlineData(true, true, "foo", true)] + [InlineData(true, false, "foo", true)] + [InlineData(null, true, null, false)] + [InlineData(null, false, null, false)] + public void CheckNullBehaviorCombination(bool? omitNullValuedAttributes, bool? allowClientOverride, string clientOverride, bool omitsNulls) + { + + NullAttributeResponseBehavior nullAttributeResponseBehavior; + if (omitNullValuedAttributes.HasValue && allowClientOverride.HasValue) + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value, allowClientOverride.Value); + }else if (omitNullValuedAttributes.HasValue) + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitNullValuedAttributes.Value); + }else if + (allowClientOverride.HasValue) + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(allowClientOverride: allowClientOverride.Value); + } + else + { + nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); + } + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupGet(m => m.Options) + .Returns(new JsonApiOptions() {NullAttributeResponseBehavior = nullAttributeResponseBehavior}); + + var httpContext = new DefaultHttpContext(); + if (clientOverride != null) + { + httpContext.Request.QueryString = new QueryString($"?omitNullValuedAttributes={clientOverride}"); + } + var httpContextAccessorMock = new Mock(); + httpContextAccessorMock.SetupGet(m => m.HttpContext).Returns(httpContext); + + var sut = new DocumentBuilderOptionsProvider(jsonApiContextMock.Object, httpContextAccessorMock.Object); + var documentBuilderOptions = sut.GetDocumentBuilderOptions(); + + Assert.Equal(omitsNulls, documentBuilderOptions.OmitNullValuedAttributes); + } + + } +} diff --git a/test/UnitTests/Builders/DocumentBuilder_Tests.cs b/test/UnitTests/Builders/DocumentBuilder_Tests.cs index 2cc4e7f7a3..7946efa058 100644 --- a/test/UnitTests/Builders/DocumentBuilder_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilder_Tests.cs @@ -16,10 +16,12 @@ public class DocumentBuilder_Tests private readonly Mock _jsonApiContextMock; private readonly PageManager _pageManager; private readonly JsonApiOptions _options; + private readonly Mock _requestMetaMock; public DocumentBuilder_Tests() { _jsonApiContextMock = new Mock(); + _requestMetaMock = new Mock(); _options = new JsonApiOptions(); @@ -141,11 +143,38 @@ public void Build_Can_Build_CustomIEnumerables() Assert.Equal(1, documents.Data.Count); } + + [Theory] + [InlineData(null,null,true)] + [InlineData(false,null,true)] + [InlineData(true,null,false)] + [InlineData(null,"foo",true)] + [InlineData(false,"foo",true)] + [InlineData(true,"foo",true)] + public void DocumentBuilderOptions(bool? omitNullValuedAttributes, + string attributeValue, + bool resultContainsAttribute) + { + var documentBuilderBehaviourMock = new Mock(); + if (omitNullValuedAttributes.HasValue) + { + documentBuilderBehaviourMock.Setup(m => m.GetDocumentBuilderOptions()) + .Returns(new DocumentBuilderOptions(omitNullValuedAttributes.Value)); + } + var documentBuilder = new DocumentBuilder(_jsonApiContextMock.Object, null, omitNullValuedAttributes.HasValue ? documentBuilderBehaviourMock.Object : null); + var document = documentBuilder.Build(new Model(){StringProperty = attributeValue}); + + Assert.Equal(resultContainsAttribute, document.Data.Attributes.ContainsKey("StringProperty")); + } + private class Model : Identifiable { [HasOne("related-model", Link.None)] public RelatedModel RelatedModel { get; set; } public int RelatedModelId { get; set; } + [Attr("StringProperty")] + public string StringProperty { get; set; } + } private class RelatedModel : Identifiable diff --git a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs index 5096cbac31..1e20c0359e 100644 --- a/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs +++ b/test/UnitTests/Serialization/JsonApiDeSerializerTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Configuration; @@ -271,6 +271,54 @@ public void Can_Deserialize_Independent_Side_Of_One_To_One_Relationship_With_Rel Assert.Equal(property, result.Property); } + [Fact] + public void Sets_The_DocumentMeta_Property_In_JsonApiContext() + { + // arrange + var contextGraphBuilder = new ContextGraphBuilder(); + contextGraphBuilder.AddResource("independents"); + contextGraphBuilder.AddResource("dependents"); + var contextGraph = contextGraphBuilder.Build(); + + var jsonApiContextMock = new Mock(); + jsonApiContextMock.SetupAllProperties(); + jsonApiContextMock.Setup(m => m.ContextGraph).Returns(contextGraph); + jsonApiContextMock.Setup(m => m.AttributesToUpdate).Returns(new Dictionary()); + + var jsonApiOptions = new JsonApiOptions(); + jsonApiContextMock.Setup(m => m.Options).Returns(jsonApiOptions); + + var genericProcessorFactoryMock = new Mock(); + + var deserializer = new JsonApiDeSerializer(jsonApiContextMock.Object, genericProcessorFactoryMock.Object); + + var property = Guid.NewGuid().ToString(); + + var content = new Document + { + Meta = new Dictionary() { {"foo", "bar"}}, + Data = new DocumentData + { + Type = "independents", + Id = "1", + Attributes = new Dictionary { { "property", property } + }, + // a common case for this is deserialization in unit tests + Relationships = new Dictionary { { "dependent", new RelationshipData { } } + } + } + }; + + var contentString = JsonConvert.SerializeObject(content); + + // act + var result = deserializer.Deserialize(contentString); + + // assert + jsonApiContextMock.VerifySet(mock => mock.DocumentMeta = content.Meta); + } + + private class TestResource : Identifiable { [Attr("complex-member")] public ComplexType ComplexMember { get; set; }