From 73a1b2e007661d34a48c5c1ce90fc5622c457f90 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 14 Feb 2020 14:51:13 +0100 Subject: [PATCH] Bugfix: Respect customizations on JsonSerializerSettings when serializing response. --- .../JsonApiSerializerBenchmarks.cs | 5 +- .../Configuration/IJsonApiOptions.cs | 3 + .../Configuration/JsonApiOptions.cs | 2 +- .../Internal/Exceptions/ErrorCollection.cs | 16 +- ...erializerSettingsNullValueHandlingScope.cs | 30 ++ .../Server/ResponseSerializer.cs | 335 +++++++++--------- .../Serialization/SerializerTestsSetup.cs | 10 +- 7 files changed, 227 insertions(+), 174 deletions(-) create mode 100644 src/JsonApiDotNetCore/Serialization/Common/JsonSerializerSettingsNullValueHandlingScope.cs diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index 3d2aa30f27..3ec09fa99c 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -1,5 +1,6 @@ using System; using BenchmarkDotNet.Attributes; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers; using JsonApiDotNetCore.Query; @@ -23,6 +24,8 @@ public class JsonApiSerializerBenchmarks public JsonApiSerializerBenchmarks() { + var options = new JsonApiOptions(); + IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(); IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph); @@ -33,7 +36,7 @@ public JsonApiSerializerBenchmarks() var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); _jsonApiSerializer = new ResponseSerializer(metaBuilderMock.Object, linkBuilderMock.Object, - includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder); + includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, options); } private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 591545f48d..0a20d624f2 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; + namespace JsonApiDotNetCore.Configuration { public interface IJsonApiOptions : ILinksConfiguration, ISerializerOptions @@ -25,6 +27,7 @@ public interface IJsonApiOptions : ILinksConfiguration, ISerializerOptions bool AllowClientGeneratedIds { get; } bool AllowCustomQueryParameters { get; set; } string Namespace { get; set; } + JsonSerializerSettings SerializerSettings { get; } } public interface ISerializerOptions diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 88e281fd5f..ad3a291ffd 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -140,7 +140,7 @@ public class JsonApiOptions : IJsonApiOptions /// public bool ValidateModelState { get; set; } - public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings() + public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; diff --git a/src/JsonApiDotNetCore/Internal/Exceptions/ErrorCollection.cs b/src/JsonApiDotNetCore/Internal/Exceptions/ErrorCollection.cs index 91e6d962da..d1667da2c9 100644 --- a/src/JsonApiDotNetCore/Internal/Exceptions/ErrorCollection.cs +++ b/src/JsonApiDotNetCore/Internal/Exceptions/ErrorCollection.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Serialization.Common; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Microsoft.AspNetCore.Mvc; @@ -20,12 +21,17 @@ public void Add(Error error) Errors.Add(error); } - public string GetJson() + public string GetJson(JsonSerializerSettings serializerSettings) { - return JsonConvert.SerializeObject(this, new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); + var beforeContractResolver = serializerSettings.ContractResolver; + serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + + using var scope = new JsonSerializerSettingsNullValueHandlingScope(serializerSettings, NullValueHandling.Ignore); + string json = JsonConvert.SerializeObject(this, serializerSettings); + + serializerSettings.ContractResolver = beforeContractResolver; + + return json; } public int GetErrorStatusCode() diff --git a/src/JsonApiDotNetCore/Serialization/Common/JsonSerializerSettingsNullValueHandlingScope.cs b/src/JsonApiDotNetCore/Serialization/Common/JsonSerializerSettingsNullValueHandlingScope.cs new file mode 100644 index 0000000000..b5b52f5013 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Common/JsonSerializerSettingsNullValueHandlingScope.cs @@ -0,0 +1,30 @@ +using System; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.Serialization.Common +{ + /// + /// Used to temporarily switch to a different value while preserving other settings. + /// After disposal, the original NullValueHandling value is restored. + /// + internal sealed class JsonSerializerSettingsNullValueHandlingScope : IDisposable + { + private readonly NullValueHandling _beforeNullValueHandling; + + public JsonSerializerSettings Settings { get; set; } + + public JsonSerializerSettingsNullValueHandlingScope(JsonSerializerSettings settings, + NullValueHandling nullValueHandling) + { + _beforeNullValueHandling = settings.NullValueHandling; + Settings = settings; + + Settings.NullValueHandling = nullValueHandling; + } + + public void Dispose() + { + Settings.NullValueHandling = _beforeNullValueHandling; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs index 4c86f29e2e..d53a278dab 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -1,164 +1,173 @@ using System; -using System.Collections; -using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Query; -using Newtonsoft.Json; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Serialization.Server.Builders; -using JsonApiDotNetCore.Internal; - -namespace JsonApiDotNetCore.Serialization.Server -{ - - /// - /// Server serializer implementation of - /// - /// - /// Because in JsonApiDotNetCore every json:api request is associated with exactly one - /// resource (the request resource, see ), - /// the serializer can leverage this information using generics. - /// See for how this is instantiated. - /// - /// Type of the resource associated with the scope of the request - /// for which this serializer is used. - public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerializer, IResponseSerializer - where TResource : class, IIdentifiable - { - public RelationshipAttribute RequestRelationship { get; set; } - private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); - private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); - private readonly IFieldsToSerialize _fieldsToSerialize; - private readonly IMetaBuilder _metaBuilder; - private readonly Type _primaryResourceType; - private readonly ILinkBuilder _linkBuilder; - private readonly IIncludedResourceObjectBuilder _includedBuilder; - - public ResponseSerializer(IMetaBuilder metaBuilder, - ILinkBuilder linkBuilder, - IIncludedResourceObjectBuilder includedBuilder, - IFieldsToSerialize fieldsToSerialize, - IResourceObjectBuilder resourceObjectBuilder) : - base(resourceObjectBuilder) - { - _fieldsToSerialize = fieldsToSerialize; - _linkBuilder = linkBuilder; - _metaBuilder = metaBuilder; - _includedBuilder = includedBuilder; - _primaryResourceType = typeof(TResource); - } - - /// - public string Serialize(object data) - { - if (data is ErrorCollection error) - return error.GetJson(); - if (data is IEnumerable entities) - return SerializeMany(entities); - return SerializeSingle((IIdentifiable)data); - } - - /// - /// Convert a single entity into a serialized - /// - /// - /// This method is set internal instead of private for easier testability. - /// - internal string SerializeSingle(IIdentifiable entity) - { - if (RequestRelationship != null) - return JsonConvert.SerializeObject(((ResponseResourceObjectBuilder)_resourceObjectBuilder).Build(entity, RequestRelationship)); - - var (attributes, relationships) = GetFieldsToSerialize(); - var document = Build(entity, attributes, relationships); - var resourceObject = document.SingleData; - if (resourceObject != null) - resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); - - AddTopLevelObjects(document); - return JsonConvert.SerializeObject(document); - - } - - private (List, List) GetFieldsToSerialize() - { - return (GetAttributesToSerialize(_primaryResourceType), GetRelationshipsToSerialize(_primaryResourceType)); - } - - /// - /// Convert a list of entities into a serialized - /// - /// - /// This method is set internal instead of private for easier testability. - /// - internal string SerializeMany(IEnumerable entities) - { - var (attributes, relationships) = GetFieldsToSerialize(); - var document = Build(entities, attributes, relationships); - foreach (ResourceObject resourceObject in (IEnumerable)document.Data) - { - var links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); - if (links == null) - break; - - resourceObject.Links = links; - } - - AddTopLevelObjects(document); - return JsonConvert.SerializeObject(document); - } - - /// - /// Gets the list of attributes to serialize for the given . - /// Note that the choice omitting null-values is not handled here, - /// but in . - /// - /// Type of entity to be serialized - /// List of allowed attributes in the serialized result - private List GetAttributesToSerialize(Type resourceType) - { - // Check the attributes cache to see if the allowed attrs for this resource type were determined before. - if (_attributesToSerializeCache.TryGetValue(resourceType, out List allowedAttributes)) - return allowedAttributes; - - // Get the list of attributes to be exposed for this type - allowedAttributes = _fieldsToSerialize.GetAllowedAttributes(resourceType); - - // add to cache so we we don't have to look this up next time. - _attributesToSerializeCache.Add(resourceType, allowedAttributes); - return allowedAttributes; - } - - /// - /// By default, the server serializer exposes all defined relationships, unless - /// in the a subset to hide was defined explicitly. - /// - /// Type of entity to be serialized - /// List of allowed relationships in the serialized result - private List GetRelationshipsToSerialize(Type resourceType) - { - // Check the relationships cache to see if the allowed attrs for this resource type were determined before. - if (_relationshipsToSerializeCache.TryGetValue(resourceType, out List allowedRelations)) - return allowedRelations; - - // Get the list of relationships to be exposed for this type - allowedRelations = _fieldsToSerialize.GetAllowedRelationships(resourceType); - // add to cache so we we don't have to look this up next time. - _relationshipsToSerializeCache.Add(resourceType, allowedRelations); - return allowedRelations; - - } - - /// - /// Adds top-level objects that are only added to a document in the case - /// of server-side serialization. - /// - private void AddTopLevelObjects(Document document) - { - document.Links = _linkBuilder.GetTopLevelLinks(); - document.Meta = _metaBuilder.GetMeta(); - document.Included = _includedBuilder.Build(); - } - } -} +using System.Collections; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Serialization.Server.Builders; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Serialization.Common; + +namespace JsonApiDotNetCore.Serialization.Server +{ + + /// + /// Server serializer implementation of + /// + /// + /// Because in JsonApiDotNetCore every json:api request is associated with exactly one + /// resource (the request resource, see ), + /// the serializer can leverage this information using generics. + /// See for how this is instantiated. + /// + /// Type of the resource associated with the scope of the request + /// for which this serializer is used. + public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerializer, IResponseSerializer + where TResource : class, IIdentifiable + { + public RelationshipAttribute RequestRelationship { get; set; } + private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); + private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); + private readonly IFieldsToSerialize _fieldsToSerialize; + private readonly IJsonApiOptions _options; + private readonly IMetaBuilder _metaBuilder; + private readonly Type _primaryResourceType; + private readonly ILinkBuilder _linkBuilder; + private readonly IIncludedResourceObjectBuilder _includedBuilder; + + public ResponseSerializer( + IMetaBuilder metaBuilder, + ILinkBuilder linkBuilder, + IIncludedResourceObjectBuilder includedBuilder, + IFieldsToSerialize fieldsToSerialize, + IResourceObjectBuilder resourceObjectBuilder, + IJsonApiOptions options) + : base(resourceObjectBuilder) + { + _fieldsToSerialize = fieldsToSerialize; + _options = options; + _linkBuilder = linkBuilder; + _metaBuilder = metaBuilder; + _includedBuilder = includedBuilder; + _primaryResourceType = typeof(TResource); + } + + /// + public string Serialize(object data) + { + if (data is ErrorCollection error) + return error.GetJson(_options.SerializerSettings); + if (data is IEnumerable entities) + return SerializeMany(entities); + return SerializeSingle((IIdentifiable)data); + } + + /// + /// Convert a single entity into a serialized + /// + /// + /// This method is set internal instead of private for easier testability. + /// + internal string SerializeSingle(IIdentifiable entity) + { + if (RequestRelationship != null) + return SerializeObject(((ResponseResourceObjectBuilder)_resourceObjectBuilder).Build(entity, RequestRelationship)); + + var (attributes, relationships) = GetFieldsToSerialize(); + var document = Build(entity, attributes, relationships); + var resourceObject = document.SingleData; + if (resourceObject != null) + resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + + AddTopLevelObjects(document); + return SerializeObject(document); + } + + private string SerializeObject(object value) + { + using var scope = new JsonSerializerSettingsNullValueHandlingScope(_options.SerializerSettings, NullValueHandling.Include); + return JsonConvert.SerializeObject(value, scope.Settings); + } + + private (List, List) GetFieldsToSerialize() + { + return (GetAttributesToSerialize(_primaryResourceType), GetRelationshipsToSerialize(_primaryResourceType)); + } + + /// + /// Convert a list of entities into a serialized + /// + /// + /// This method is set internal instead of private for easier testability. + /// + internal string SerializeMany(IEnumerable entities) + { + var (attributes, relationships) = GetFieldsToSerialize(); + var document = Build(entities, attributes, relationships); + foreach (ResourceObject resourceObject in (IEnumerable)document.Data) + { + var links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); + if (links == null) + break; + + resourceObject.Links = links; + } + + AddTopLevelObjects(document); + return SerializeObject(document); + } + + /// + /// Gets the list of attributes to serialize for the given . + /// Note that the choice omitting null-values is not handled here, + /// but in . + /// + /// Type of entity to be serialized + /// List of allowed attributes in the serialized result + private List GetAttributesToSerialize(Type resourceType) + { + // Check the attributes cache to see if the allowed attrs for this resource type were determined before. + if (_attributesToSerializeCache.TryGetValue(resourceType, out List allowedAttributes)) + return allowedAttributes; + + // Get the list of attributes to be exposed for this type + allowedAttributes = _fieldsToSerialize.GetAllowedAttributes(resourceType); + + // add to cache so we we don't have to look this up next time. + _attributesToSerializeCache.Add(resourceType, allowedAttributes); + return allowedAttributes; + } + + /// + /// By default, the server serializer exposes all defined relationships, unless + /// in the a subset to hide was defined explicitly. + /// + /// Type of entity to be serialized + /// List of allowed relationships in the serialized result + private List GetRelationshipsToSerialize(Type resourceType) + { + // Check the relationships cache to see if the allowed attrs for this resource type were determined before. + if (_relationshipsToSerializeCache.TryGetValue(resourceType, out List allowedRelations)) + return allowedRelations; + + // Get the list of relationships to be exposed for this type + allowedRelations = _fieldsToSerialize.GetAllowedRelationships(resourceType); + // add to cache so we we don't have to look this up next time. + _relationshipsToSerializeCache.Add(resourceType, allowedRelations); + return allowedRelations; + + } + + /// + /// Adds top-level objects that are only added to a document in the case + /// of server-side serialization. + /// + private void AddTopLevelObjects(Document document) + { + document.Links = _linkBuilder.GetTopLevelLinks(); + document.Meta = _metaBuilder.GetMeta(); + document.Included = _includedBuilder.Build(); + } + } +} diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index de4f00825c..4dbb810cf6 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -1,6 +1,7 @@ -using System; -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; @@ -49,7 +50,8 @@ protected ResponseSerializer GetResponseSerializer(List(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder); + var options = new JsonApiOptions(); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, options); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(List> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) @@ -137,4 +139,4 @@ public TestDocumentBuilder(IResourceObjectBuilder resourceObjectBuilder) : base( } } } -} \ No newline at end of file +}