Skip to content

Feat/#226: Add DocumentBuilderOptions allowing omission of null attributes #227

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 2 commits into from
Jan 30, 2018
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
28 changes: 14 additions & 14 deletions JsonApiDotnetCore.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
31 changes: 18 additions & 13 deletions src/JsonApiDotNetCore/Builders/DocumentBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions src/JsonApiDotNetCore/Builders/DocumentBuilderOptions.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
33 changes: 33 additions & 0 deletions src/JsonApiDotNetCore/Builders/DocumentBuilderOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
11 changes: 11 additions & 0 deletions src/JsonApiDotNetCore/Builders/IDocumentBuilderOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace JsonApiDotNetCore.Builders
{
public interface IDocumentBuilderOptionsProvider
{
DocumentBuilderOptions GetDocumentBuilderOptions();
}
}
4 changes: 4 additions & 0 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +30,7 @@ public IContractResolver JsonContractResolver
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new DasherizedResolver()
};

internal IContextGraphBuilder ContextGraphBuilder { get; } = new ContextGraphBuilder();

public void BuildContextGraph<TContext>(Action<IContextGraphBuilder> builder) where TContext : DbContext
Expand All @@ -49,4 +51,6 @@ public void BuildContextGraph(Action<IContextGraphBuilder> builder)
ContextGraph = ContextGraphBuilder.Build();
}
}


}
Original file line number Diff line number Diff line change
@@ -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; }
// ...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public static void AddJsonApiInternals(
services.AddScoped<IQueryAccessor, QueryAccessor>();
services.AddScoped<IQueryParser, QueryParser>();
services.AddScoped<IControllerContext, Services.ControllerContext>();
services.AddScoped<IDocumentBuilderOptionsProvider, DocumentBuilderOptionsProvider>();
}

public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions)
Expand Down
3 changes: 2 additions & 1 deletion src/JsonApiDotNetCore/Serialization/JsonApiDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public object Deserialize(string requestBody)
try
{
var document = JsonConvert.DeserializeObject<Document>(requestBody);
_jsonApiContext.DocumentMeta = document.Meta;
var entity = DocumentToObject(document.Data);
return entity;
}
Expand Down Expand Up @@ -222,4 +223,4 @@ private object SetHasManyRelationship(object entity,
return entity;
}
}
}
}
2 changes: 2 additions & 0 deletions src/JsonApiDotNetCore/Services/IJsonApiContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public interface IJsonApiContext
Dictionary<AttrAttribute, object> AttributesToUpdate { get; set; }
Dictionary<RelationshipAttribute, object> RelationshipsToUpdate { get; set; }
Type ControllerType { get; set; }
Dictionary<string, object> DocumentMeta { get; set; }

TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute;
}
}
1 change: 1 addition & 0 deletions src/JsonApiDotNetCore/Services/JsonApiContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public JsonApiContext(
public Dictionary<AttrAttribute, object> AttributesToUpdate { get; set; } = new Dictionary<AttrAttribute, object>();
public Dictionary<RelationshipAttribute, object> RelationshipsToUpdate { get; set; } = new Dictionary<RelationshipAttribute, object>();
public Type ControllerType { get; set; }
public Dictionary<string, object> DocumentMeta { get; set; }

public IJsonApiContext ApplyContext<T>(object controller)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Startup> _fixture;
private readonly AppDbContext _dbContext;
private readonly TodoItem _todoItem;

public NullValuedAttributeHandlingTests(TestFixture<Startup> fixture)
{
_fixture = fixture;
_dbContext = fixture.GetService<AppDbContext>();
_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>();
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<Document>(body);

// assert. does response contain a null valued attribute
Assert.Equal(omitsNulls, !deserializeBody.Data.Attributes.ContainsKey("description"));

}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public void AddJsonApiInternals_Adds_All_Required_Services()
Assert.NotNull(provider.GetService<IJsonApiReader>());
Assert.NotNull(provider.GetService<IJsonApiDeSerializer>());
Assert.NotNull(provider.GetService<IGenericProcessorFactory>());
Assert.NotNull(provider.GetService<IDocumentBuilderOptionsProvider>());
Assert.NotNull(provider.GetService(typeof(GenericProcessor<TodoItem>)));
}
}
Expand Down
Loading