From b82f91db735cb9c625303945dc68c5d728cdaabe Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 9 Apr 2020 11:40:12 +0200 Subject: [PATCH 01/11] Removed ISerializerOptions --- .../Configuration/IJsonApiOptions.cs | 23 ++++++++++++++----- .../Configuration/JsonApiOptions.cs | 22 ++++++++++-------- .../OmitDefaultService.cs | 4 ++-- .../QueryParameterServices/OmitNullService.cs | 4 ++-- .../Common/ResourceObjectBuilderSettings.cs | 5 ++-- .../OmitAttributeIfValueIsNullTests.cs | 23 ++++++++----------- .../OmitDefaultServiceTests.cs | 3 ++- .../QueryParameters/OmitNullServiceTests.cs | 3 ++- 8 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index c20e2eaf7f..2e85d1f0f3 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,9 +1,10 @@ using System; using JsonApiDotNetCore.Models.JsonApiDocuments; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Configuration { - public interface IJsonApiOptions : ILinksConfiguration, ISerializerOptions + public interface IJsonApiOptions : ILinksConfiguration { /// /// Whether or not stack traces should be serialized in objects. @@ -33,11 +34,21 @@ public interface IJsonApiOptions : ILinksConfiguration, ISerializerOptions bool AllowClientGeneratedIds { get; } bool AllowCustomQueryStringParameters { get; set; } string Namespace { get; set; } - } - public interface ISerializerOptions - { - NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } - DefaultAttributeResponseBehavior DefaultAttributeResponseBehavior { get; set; } + /// + /// Determines whether the serialization setting can be overridden by a query string parameter. + /// + bool AllowOmitNullQueryStringOverride { get; set; } + + /// + /// Determines whether the serialization setting can be overridden by a query string parameter. + /// + bool AllowOmitDefaultQueryStringOverride { get; set; } + + // TODO: Replace these with JsonSerializerSettings usage. + bool SerializerOmitAttributeIfValueIsNull { get; set; } + bool SerializerOmitAttributeIfValueIsDefault { get; set; } + + JsonSerializerSettings SerializerSettings { get; } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 6466216a36..19d68e173d 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -52,6 +52,18 @@ public class JsonApiOptions : IJsonApiOptions /// public string Namespace { get; set; } + /// + public bool AllowOmitNullQueryStringOverride { get; set; } + + /// + public bool AllowOmitDefaultQueryStringOverride { get; set; } + + /// + public bool SerializerOmitAttributeIfValueIsNull { get; set; } + + /// + public bool SerializerOmitAttributeIfValueIsDefault { get; set; } + /// /// The default page size for all resources. The value zero means: no paging. /// @@ -107,16 +119,6 @@ public class JsonApiOptions : IJsonApiOptions /// public bool AllowCustomQueryStringParameters { get; set; } - /// - /// The default behavior for serializing attributes that contain null. - /// - public NullAttributeResponseBehavior NullAttributeResponseBehavior { get; set; } - - /// - /// The default behavior for serializing attributes that contain their types' default value. - /// - public DefaultAttributeResponseBehavior DefaultAttributeResponseBehavior { get; set; } - /// /// Whether or not to validate model state. /// diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs index 80d26127d4..4163644fe4 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs @@ -12,7 +12,7 @@ public class OmitDefaultService : QueryParameterService, IOmitDefaultService public OmitDefaultService(IJsonApiOptions options) { - OmitAttributeIfValueIsDefault = options.DefaultAttributeResponseBehavior.OmitAttributeIfValueIsDefault; + OmitAttributeIfValueIsDefault = options.SerializerOmitAttributeIfValueIsDefault; _options = options; } @@ -21,7 +21,7 @@ public OmitDefaultService(IJsonApiOptions options) public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) { - return _options.DefaultAttributeResponseBehavior.AllowQueryStringOverride && + return _options.AllowOmitDefaultQueryStringOverride && !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.OmitDefault); } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs index 3fb26decf5..ec6b1dcf7a 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs @@ -12,7 +12,7 @@ public class OmitNullService : QueryParameterService, IOmitNullService public OmitNullService(IJsonApiOptions options) { - OmitAttributeIfValueIsNull = options.NullAttributeResponseBehavior.OmitAttributeIfValueIsNull; + OmitAttributeIfValueIsNull = options.SerializerOmitAttributeIfValueIsNull; _options = options; } @@ -22,7 +22,7 @@ public OmitNullService(IJsonApiOptions options) /// public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) { - return _options.NullAttributeResponseBehavior.AllowQueryStringOverride && + return _options.AllowOmitNullQueryStringOverride && !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.OmitNull); } diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs index e758366040..2be257467e 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs @@ -1,3 +1,4 @@ +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; namespace JsonApiDotNetCore.Serialization @@ -19,7 +20,7 @@ public ResourceObjectBuilderSettings(bool omitAttributeIfValueIsNull = false, bo /// /// Prevent attributes with null values from being included in the response. /// This property is internal and if you want to enable this behavior, you - /// should do so on the . + /// should do so on the . /// /// /// @@ -31,7 +32,7 @@ public ResourceObjectBuilderSettings(bool omitAttributeIfValueIsNull = false, bo /// /// Prevent attributes with default values from being included in the response. /// This property is internal and if you want to enable this behavior, you - /// should do so on the . + /// should do so on the . /// /// /// diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs index ec98d103fd..09eb775674 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs @@ -66,23 +66,18 @@ public Task DisposeAsync() public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, bool? allowQueryStringOverride, string queryStringOverride, bool expectNullsMissing) { - - // Override some null handling options - NullAttributeResponseBehavior nullAttributeResponseBehavior; - if (omitAttributeIfValueIsNull.HasValue && allowQueryStringOverride.HasValue) - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitAttributeIfValueIsNull.Value, allowQueryStringOverride.Value); - else if (omitAttributeIfValueIsNull.HasValue) - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(omitAttributeIfValueIsNull.Value); - else if (allowQueryStringOverride.HasValue) - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(allowQueryStringOverride: allowQueryStringOverride.Value); - else - nullAttributeResponseBehavior = new NullAttributeResponseBehavior(); - var jsonApiOptions = _fixture.GetService(); - jsonApiOptions.NullAttributeResponseBehavior = nullAttributeResponseBehavior; + if (omitAttributeIfValueIsNull != null) + { + jsonApiOptions.SerializerOmitAttributeIfValueIsNull = omitAttributeIfValueIsNull.Value; + } + if (allowQueryStringOverride != null) + { + jsonApiOptions.AllowOmitNullQueryStringOverride = allowQueryStringOverride.Value; + } var httpMethod = new HttpMethod("GET"); - var queryString = allowQueryStringOverride.HasValue + var queryString = allowQueryStringOverride != null ? $"&omitNull={queryStringOverride}" : ""; var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}"; diff --git a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs b/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs index 75ee3517ff..b412ffecda 100644 --- a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs +++ b/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs @@ -15,7 +15,8 @@ public OmitDefaultService GetService(bool @default, bool @override) { var options = new JsonApiOptions { - DefaultAttributeResponseBehavior = new DefaultAttributeResponseBehavior(@default, @override) + SerializerOmitAttributeIfValueIsDefault = @default, + AllowOmitDefaultQueryStringOverride = @override }; return new OmitDefaultService(options); diff --git a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs b/test/UnitTests/QueryParameters/OmitNullServiceTests.cs index bb23bdd4b7..dd13a711a1 100644 --- a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs +++ b/test/UnitTests/QueryParameters/OmitNullServiceTests.cs @@ -15,7 +15,8 @@ public OmitNullService GetService(bool @default, bool @override) { var options = new JsonApiOptions { - NullAttributeResponseBehavior = new NullAttributeResponseBehavior(@default, @override) + SerializerOmitAttributeIfValueIsNull = @default, + AllowOmitNullQueryStringOverride = @override }; return new OmitNullService(options); From 67e1d3eacae402e42606eaaa0694c17e0b69a74d Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 9 Apr 2020 11:50:01 +0200 Subject: [PATCH 02/11] Replaced temporary properties on options with SerializerSettings usage --- src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs | 4 ---- src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs | 7 ------- .../QueryParameterServices/OmitDefaultService.cs | 3 ++- .../QueryParameterServices/OmitNullService.cs | 3 ++- .../Extensibility/OmitAttributeIfValueIsNullTests.cs | 4 +++- test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs | 6 +++++- test/UnitTests/QueryParameters/OmitNullServiceTests.cs | 6 +++++- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 2e85d1f0f3..c50468eb5d 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -45,10 +45,6 @@ public interface IJsonApiOptions : ILinksConfiguration /// bool AllowOmitDefaultQueryStringOverride { get; set; } - // TODO: Replace these with JsonSerializerSettings usage. - bool SerializerOmitAttributeIfValueIsNull { get; set; } - bool SerializerOmitAttributeIfValueIsDefault { get; set; } - JsonSerializerSettings SerializerSettings { get; } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 19d68e173d..7429dcea9e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -57,12 +57,6 @@ public class JsonApiOptions : IJsonApiOptions /// public bool AllowOmitDefaultQueryStringOverride { get; set; } - - /// - public bool SerializerOmitAttributeIfValueIsNull { get; set; } - - /// - public bool SerializerOmitAttributeIfValueIsDefault { get; set; } /// /// The default page size for all resources. The value zero means: no paging. @@ -131,7 +125,6 @@ public class JsonApiOptions : IJsonApiOptions public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore }; } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs index 4163644fe4..13061aa066 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs @@ -2,6 +2,7 @@ using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Exceptions; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Query { @@ -12,7 +13,7 @@ public class OmitDefaultService : QueryParameterService, IOmitDefaultService public OmitDefaultService(IJsonApiOptions options) { - OmitAttributeIfValueIsDefault = options.SerializerOmitAttributeIfValueIsDefault; + OmitAttributeIfValueIsDefault = options.SerializerSettings.DefaultValueHandling == DefaultValueHandling.Ignore; _options = options; } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs index ec6b1dcf7a..a9008ba077 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs @@ -2,6 +2,7 @@ using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Exceptions; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Query { @@ -12,7 +13,7 @@ public class OmitNullService : QueryParameterService, IOmitNullService public OmitNullService(IJsonApiOptions options) { - OmitAttributeIfValueIsNull = options.SerializerOmitAttributeIfValueIsNull; + OmitAttributeIfValueIsNull = options.SerializerSettings.NullValueHandling == NullValueHandling.Ignore; _options = options; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs index 09eb775674..9e22588693 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs @@ -69,7 +69,9 @@ public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, var jsonApiOptions = _fixture.GetService(); if (omitAttributeIfValueIsNull != null) { - jsonApiOptions.SerializerOmitAttributeIfValueIsNull = omitAttributeIfValueIsNull.Value; + jsonApiOptions.SerializerSettings.NullValueHandling = omitAttributeIfValueIsNull.Value + ? NullValueHandling.Ignore + : NullValueHandling.Include; } if (allowQueryStringOverride != null) { diff --git a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs b/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs index b412ffecda..c1d29c7896 100644 --- a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs +++ b/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Query; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; using Xunit; namespace UnitTests.QueryParameters @@ -15,7 +16,10 @@ public OmitDefaultService GetService(bool @default, bool @override) { var options = new JsonApiOptions { - SerializerOmitAttributeIfValueIsDefault = @default, + SerializerSettings = + { + DefaultValueHandling = @default ? DefaultValueHandling.Ignore : DefaultValueHandling.Include + }, AllowOmitDefaultQueryStringOverride = @override }; diff --git a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs b/test/UnitTests/QueryParameters/OmitNullServiceTests.cs index dd13a711a1..740404c90c 100644 --- a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs +++ b/test/UnitTests/QueryParameters/OmitNullServiceTests.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Query; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; using Xunit; namespace UnitTests.QueryParameters @@ -15,7 +16,10 @@ public OmitNullService GetService(bool @default, bool @override) { var options = new JsonApiOptions { - SerializerOmitAttributeIfValueIsNull = @default, + SerializerSettings = + { + NullValueHandling = @default ? NullValueHandling.Ignore : NullValueHandling.Include + }, AllowOmitNullQueryStringOverride = @override }; From a28fef3cc6ce9033f002537c0f555617633e8f09 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 9 Apr 2020 15:15:19 +0200 Subject: [PATCH 03/11] Replaced usages of JsonConvert.SerializeObject to respect custom serializer settings --- .../JsonApiSerializerBenchmarks.cs | 3 +- .../Configuration/JsonApiOptions.cs | 4 +- .../Extensions/JsonSerializerExtensions.cs | 36 +++++++++++++ .../Middleware/CurrentRequestMiddleware.cs | 29 ++++++++--- .../Models/JsonApiDocuments/ExposableData.cs | 8 +-- .../Serialization/Client/RequestSerializer.cs | 18 +++++-- .../Serialization/Common/DocumentBuilder.cs | 17 +++++++ .../Server/ResponseSerializer.cs | 50 ++++++------------- .../Acceptance/Spec/ContentNegotiation.cs | 5 ++ .../CurrentRequestMiddlewareTests.cs | 3 +- .../Serialization/SerializerTestsSetup.cs | 3 +- 11 files changed, 120 insertions(+), 56 deletions(-) create mode 100644 src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index 5493c3d54f..d94ded2216 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.Graph; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers; @@ -34,7 +35,7 @@ public JsonApiSerializerBenchmarks() var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); _jsonApiSerializer = new ResponseSerializer(metaBuilderMock.Object, linkBuilderMock.Object, - includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter()); + includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter(), new JsonApiOptions()); } private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 7429dcea9e..acf5121978 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -123,8 +123,6 @@ public class JsonApiOptions : IJsonApiOptions /// public bool ValidateModelState { get; set; } - public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings - { - }; + public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings(); } } diff --git a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs new file mode 100644 index 0000000000..9c224bbfa8 --- /dev/null +++ b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs @@ -0,0 +1,36 @@ +using JsonApiDotNetCore.Graph; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.Extensions +{ + internal static class JsonSerializerExtensions + { + public static void ApplyErrorSettings(this JsonSerializer jsonSerializer, IResourceNameFormatter formatter) + { + jsonSerializer.NullValueHandling = NullValueHandling.Ignore; + jsonSerializer.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new NewtonsoftNamingStrategyAdapter(formatter) + }; + } + + private sealed class NewtonsoftNamingStrategyAdapter : NamingStrategy + { + private readonly IResourceNameFormatter _formatter; + + public NewtonsoftNamingStrategyAdapter(IResourceNameFormatter formatter) + { + _formatter = formatter; + + ProcessDictionaryKeys = true; + ProcessExtensionDataNames = true; + } + + protected override string ResolvePropertyName(string name) + { + return _formatter.ApplyCasingConvention(name); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs index ff19a00e97..48b152eb45 100644 --- a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs @@ -4,6 +4,8 @@ using System.Net; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers.Contracts; @@ -27,6 +29,7 @@ public sealed class CurrentRequestMiddleware private IJsonApiOptions _options; private RouteValueDictionary _routeValues; private IControllerResourceMapping _controllerResourceMapping; + private IResourceNameFormatter _formatter; public CurrentRequestMiddleware(RequestDelegate next) { @@ -37,13 +40,15 @@ public async Task Invoke(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, ICurrentRequest currentRequest, - IResourceGraph resourceGraph) + IResourceGraph resourceGraph, + IResourceNameFormatter formatter) { _httpContext = httpContext; _currentRequest = currentRequest; _controllerResourceMapping = controllerResourceMapping; _resourceGraph = resourceGraph; _options = options; + _formatter = formatter; _routeValues = httpContext.GetRouteData().Values; var requestResource = GetCurrentEntity(); if (requestResource != null) @@ -135,7 +140,7 @@ private async Task IsValidAsync() return await IsValidContentTypeHeaderAsync(_httpContext) && await IsValidAcceptHeaderAsync(_httpContext); } - private static async Task IsValidContentTypeHeaderAsync(HttpContext context) + private async Task IsValidContentTypeHeaderAsync(HttpContext context) { var contentType = context.Request.ContentType; if (contentType != null && ContainsMediaTypeParameters(contentType)) @@ -151,7 +156,7 @@ private static async Task IsValidContentTypeHeaderAsync(HttpContext contex return true; } - private static async Task IsValidAcceptHeaderAsync(HttpContext context) + private async Task IsValidAcceptHeaderAsync(HttpContext context) { if (context.Request.Headers.TryGetValue(HeaderConstants.AcceptHeader, out StringValues acceptHeaders) == false) return true; @@ -194,14 +199,24 @@ private static bool ContainsMediaTypeParameters(string mediaType) ); } - private static async Task FlushResponseAsync(HttpContext context, Error error) + private async Task FlushResponseAsync(HttpContext context, Error error) { context.Response.StatusCode = (int) error.StatusCode; - string responseBody = JsonConvert.SerializeObject(new ErrorDocument(error)); - await using (var writer = new StreamWriter(context.Response.Body)) + JsonSerializer serializer = JsonSerializer.CreateDefault(_options.SerializerSettings); + serializer.ApplyErrorSettings(_formatter); + + // https://github.com/JamesNK/Newtonsoft.Json/issues/1193 + await using (var stream = new MemoryStream()) { - await writer.WriteAsync(responseBody); + await using (var streamWriter = new StreamWriter(stream, leaveOpen: true)) + { + using var jsonWriter = new JsonTextWriter(streamWriter); + serializer.Serialize(jsonWriter, new ErrorDocument(error)); + } + + stream.Seek(0, SeekOrigin.Begin); + await stream.CopyToAsync(context.Response.Body); } context.Response.Body.Flush(); diff --git a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs index 1b7ac1c9de..cf09483775 100644 --- a/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs +++ b/src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs @@ -45,13 +45,13 @@ public bool ShouldSerializeData() public List ManyData { get; private set; } /// - /// Internally used to indicate if the document's primary data is - /// "single" or "many". + /// Used to indicate if the document's primary data is "single" or "many". /// - internal bool IsManyData { get; private set; } + [JsonIgnore] + public bool IsManyData { get; private set; } /// - /// Internally used to indicate if the document's primary data is + /// Internally used to indicate if the document's primary data /// should still be serialized when it's value is null. This is used when /// a single resource is requested but not present (eg /articles/1/author). /// diff --git a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs index 94fd06cb39..ad2d470447 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -27,12 +27,16 @@ public RequestSerializer(IResourceGraph resourceGraph, public string Serialize(IIdentifiable entity) { if (entity == null) - return JsonConvert.SerializeObject(Build((IIdentifiable) null, new List(), new List())); + { + var empty = Build((IIdentifiable) null, new List(), new List()); + return SerializeObject(empty, new JsonSerializerSettings()); + } _currentTargetedResource = entity.GetType(); var document = Build(entity, GetAttributesToSerialize(entity), GetRelationshipsToSerialize(entity)); _currentTargetedResource = null; - return JsonConvert.SerializeObject(document); + + return SerializeObject(document, new JsonSerializerSettings()); } /// @@ -44,15 +48,19 @@ public string Serialize(IEnumerable entities) entity = item; break; } + if (entity == null) - return JsonConvert.SerializeObject(Build(entities, new List(), new List())); + { + var result = Build(entities, new List(), new List()); + return SerializeObject(result, new JsonSerializerSettings()); + } _currentTargetedResource = entity.GetType(); var attributes = GetAttributesToSerialize(entity); var relationships = GetRelationshipsToSerialize(entity); var document = Build(entities, attributes, relationships); _currentTargetedResource = null; - return JsonConvert.SerializeObject(document); + return SerializeObject(document, new JsonSerializerSettings()); } /// diff --git a/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs index 9f31e18e2c..11273f99ea 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/DocumentBuilder.cs @@ -1,6 +1,9 @@ +using System; using System.Collections; using System.Collections.Generic; +using System.IO; using JsonApiDotNetCore.Models; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -49,5 +52,19 @@ protected Document Build(IEnumerable entities, List attributes, L return new Document { Data = data }; } + + protected string SerializeObject(object value, JsonSerializerSettings defaultSettings, Action changeSerializer = null) + { + JsonSerializer serializer = JsonSerializer.CreateDefault(defaultSettings); + changeSerializer?.Invoke(serializer); + + using var stringWriter = new StringWriter(); + using (var jsonWriter = new JsonTextWriter(stringWriter)) + { + serializer.Serialize(jsonWriter, value); + } + + return stringWriter.ToString(); + } } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs index 3891e31cd0..055bc8c72c 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -1,13 +1,14 @@ using System; using System.Collections; using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Models; using Newtonsoft.Json; using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Serialization.Server.Builders; using JsonApiDotNetCore.Models.JsonApiDocuments; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Serialization.Server { @@ -30,34 +31,29 @@ public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerial private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); private readonly IFieldsToSerialize _fieldsToSerialize; + private readonly IResourceNameFormatter _formatter; + private readonly IJsonApiOptions _options; private readonly IMetaBuilder _metaBuilder; private readonly Type _primaryResourceType; private readonly ILinkBuilder _linkBuilder; private readonly IIncludedResourceObjectBuilder _includedBuilder; - private readonly JsonSerializerSettings _errorSerializerSettings; public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, - IResourceNameFormatter formatter) + IResourceNameFormatter formatter, + IJsonApiOptions options) : base(resourceObjectBuilder) { _fieldsToSerialize = fieldsToSerialize; + _formatter = formatter; + _options = options; _linkBuilder = linkBuilder; _metaBuilder = metaBuilder; _includedBuilder = includedBuilder; _primaryResourceType = typeof(TResource); - - _errorSerializerSettings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new NewtonsoftNamingStrategyAdapter(formatter) - } - }; } /// @@ -72,7 +68,7 @@ public string Serialize(object data) private string SerializeErrorDocument(ErrorDocument errorDocument) { - return JsonConvert.SerializeObject(errorDocument, _errorSerializerSettings); + return SerializeObject(errorDocument, _options.SerializerSettings, serializer => { serializer.ApplyErrorSettings(_formatter); }); } /// @@ -84,7 +80,10 @@ private string SerializeErrorDocument(ErrorDocument errorDocument) internal string SerializeSingle(IIdentifiable entity) { if (RequestRelationship != null && entity != null) - return JsonConvert.SerializeObject(((ResponseResourceObjectBuilder)_resourceObjectBuilder).Build(entity, RequestRelationship)); + { + var relationship = ((ResponseResourceObjectBuilder)_resourceObjectBuilder).Build(entity, RequestRelationship); + return SerializeObject(relationship, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); + } var (attributes, relationships) = GetFieldsToSerialize(); var document = Build(entity, attributes, relationships); @@ -93,8 +92,8 @@ internal string SerializeSingle(IIdentifiable entity) resourceObject.Links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); AddTopLevelObjects(document); - return JsonConvert.SerializeObject(document); + return SerializeObject(document, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); } private (List, List) GetFieldsToSerialize() @@ -122,7 +121,8 @@ internal string SerializeMany(IEnumerable entities) } AddTopLevelObjects(document); - return JsonConvert.SerializeObject(document); + + return SerializeObject(document, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); } /// @@ -176,23 +176,5 @@ private void AddTopLevelObjects(Document document) document.Meta = _metaBuilder.GetMeta(); document.Included = _includedBuilder.Build(); } - - private sealed class NewtonsoftNamingStrategyAdapter : NamingStrategy - { - private readonly IResourceNameFormatter _formatter; - - public NewtonsoftNamingStrategyAdapter(IResourceNameFormatter formatter) - { - _formatter = formatter; - - ProcessDictionaryKeys = true; - ProcessExtensionDataNames = true; - } - - protected override string ResolvePropertyName(string name) - { - return _formatter.ApplyCasingConvention(name); - } - } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs index 930b301fda..0f9b6db262 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/ContentNegotiation.cs @@ -60,6 +60,11 @@ public async Task Server_Responds_415_With_MediaType_Parameters() Assert.Equal(HttpStatusCode.UnsupportedMediaType, errorDocument.Errors[0].StatusCode); Assert.Equal("The specified Content-Type header value is not supported.", errorDocument.Errors[0].Title); Assert.Equal("Please specify 'application/vnd.api+json' for the Content-Type header value.", errorDocument.Errors[0].Detail); + + Assert.Equal( + @"{""errors"":[{""id"":""" + errorDocument.Errors[0].Id + + @""",""status"":""415"",""title"":""The specified Content-Type header value is not supported."",""detail"":""Please specify 'application/vnd.api+json' for the Content-Type header value.""}]}", + body); } [Fact] diff --git a/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs b/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs index 73c368215f..19dfdbcef7 100644 --- a/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs +++ b/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Graph; using Xunit; namespace UnitTests.Middleware @@ -87,7 +88,7 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) var options = holder.Options.Object; var currentRequest = holder.CurrentRequest; var resourceGraph = holder.ResourceGraph.Object; - return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, currentRequest, resourceGraph); + return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, currentRequest, resourceGraph, new CamelCaseFormatter()); } private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", string action = "", string id =null, Type relType = null) { diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 5796d3fc4c..d98de0e4d5 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Links; @@ -46,7 +47,7 @@ protected ResponseSerializer GetResponseSerializer(List(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter()); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter(), new JsonApiOptions()); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(List> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) From 990762f93b87b86b0face57c4085a63bb53d88b1 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 9 Apr 2020 18:21:57 +0200 Subject: [PATCH 04/11] Replaced IResourceNameFormatter with options.SerializerSettings to control naming convention --- benchmarks/DependencyFactory.cs | 5 +- benchmarks/Query/QueryParserBenchmarks.cs | 2 +- .../JsonApiDeserializerBenchmarks.cs | 4 +- .../JsonApiSerializerBenchmarks.cs | 5 +- .../Startups/KebabCaseStartup.cs | 24 +++++++ .../Startups/NoDefaultPageSizeStartup.cs | 23 ++----- .../Startups/Startup.cs | 23 ++++--- .../Builders/IResourceGraphBuilder.cs | 4 -- .../Builders/JsonApiApplicationBuilder.cs | 17 +++-- .../Builders/ResourceGraphBuilder.cs | 43 +++++++----- .../Configuration/IJsonApiOptions.cs | 13 ++++ .../Configuration/JsonApiOptions.cs | 10 ++- .../EntityFrameworkCoreExtension.cs | 2 +- .../Extensions/JsonSerializerExtensions.cs | 27 ++------ .../Graph/ResourceIdMapper.cs | 2 +- .../Graph/ResourceNameFormatter.cs | 42 ++++++++++++ .../CamelCaseFormatter.cs | 37 ---------- .../IResourceNameFormatter.cs | 27 -------- .../KebabCaseFormatter.cs | 67 ------------------- .../ResourceNameFormatterBase.cs | 42 ------------ .../Internal/DefaultRoutingConvention.cs | 30 +++++---- .../Middleware/CurrentRequestMiddleware.cs | 8 +-- .../Server/ResponseSerializer.cs | 5 +- .../ServiceDiscoveryFacadeTests.cs | 9 ++- .../Data/EntityRepositoryTests.cs | 3 +- .../Spec/DeeplyNestedInclusionTests.cs | 5 +- .../Acceptance/Spec/EndToEndTest.cs | 6 +- .../Acceptance/Spec/SparseFieldSetTests.cs | 6 +- .../Acceptance/TestFixture.cs | 6 +- .../Factories/KebabCaseApplicationFactory.cs | 10 +-- test/NoEntityFrameworkTests/TestFixture.cs | 7 +- .../Builders/ContextGraphBuilder_Tests.cs | 39 ++--------- .../DocumentBuilderBehaviour_Tests.cs | 2 + test/UnitTests/Builders/MetaBuilderTests.cs | 2 + .../Internal/ResourceGraphBuilder_Tests.cs | 5 +- .../CurrentRequestMiddlewareTests.cs | 3 +- test/UnitTests/Models/ConstructionTests.cs | 3 +- .../Models/ResourceDefinitionTests.cs | 3 +- .../QueryParametersUnitTestCollection.cs | 7 +- .../UnitTests/ResourceHooks/DiscoveryTests.cs | 9 +-- .../ResourceHooks/ResourceHooksTestsSetup.cs | 7 +- .../SerializationTestsSetupBase.cs | 7 +- .../Serialization/SerializerTestsSetup.cs | 2 +- .../Services/EntityResourceService_Tests.cs | 3 +- 44 files changed, 248 insertions(+), 358 deletions(-) create mode 100644 src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs create mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs delete mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs delete mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs delete mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs delete mode 100644 src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 0c268bc7c5..651e8f2422 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Query; @@ -9,9 +10,9 @@ namespace Benchmarks { internal static class DependencyFactory { - public static IResourceGraph CreateResourceGraph() + public static IResourceGraph CreateResourceGraph(IJsonApiOptions options) { - IResourceGraphBuilder builder = new ResourceGraphBuilder(); + IResourceGraphBuilder builder = new ResourceGraphBuilder(options); builder.AddResource(BenchmarkResourcePublicNames.Type); return builder.Build(); } diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index 0bf78b34a1..ba8050717b 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -23,7 +23,7 @@ public class QueryParserBenchmarks public QueryParserBenchmarks() { IJsonApiOptions options = new JsonApiOptions(); - IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(); + IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); var currentRequest = new CurrentRequest(); currentRequest.SetRequestResource(resourceGraph.GetResourceContext(typeof(BenchmarkResource))); diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 30742c5440..3c041b08ce 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using BenchmarkDotNet.Attributes; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; @@ -32,7 +33,8 @@ public class JsonApiDeserializerBenchmarks public JsonApiDeserializerBenchmarks() { - IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(); + var options = new JsonApiOptions(); + IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); var targetedFields = new TargetedFields(); _jsonApiDeserializer = new RequestDeserializer(resourceGraph, targetedFields); diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index d94ded2216..c9babb5d76 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -25,7 +25,8 @@ public class JsonApiSerializerBenchmarks public JsonApiSerializerBenchmarks() { - IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(); + var options = new JsonApiOptions(); + IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); IFieldsToSerialize fieldsToSerialize = CreateFieldsToSerialize(resourceGraph); var metaBuilderMock = new Mock>(); @@ -35,7 +36,7 @@ public JsonApiSerializerBenchmarks() var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); _jsonApiSerializer = new ResponseSerializer(metaBuilderMock.Object, linkBuilderMock.Object, - includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter(), new JsonApiOptions()); + includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, options); } private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs new file mode 100644 index 0000000000..c6d8b20804 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs @@ -0,0 +1,24 @@ +using JsonApiDotNetCore.Configuration; +using Microsoft.AspNetCore.Hosting; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCoreExample +{ + /// + /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0 + /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373. + /// + public sealed class KebabCaseStartup : Startup + { + public KebabCaseStartup(IWebHostEnvironment env) : base(env) + { + } + + protected override void ConfigureJsonApiOptions(JsonApiOptions options) + { + base.ConfigureJsonApiOptions(options); + + ((DefaultContractResolver)options.SerializerSettings.ContractResolver).NamingStrategy = new KebabCaseNamingStrategy(); + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs index 33a986805e..837515b333 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs @@ -1,9 +1,5 @@ using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using JsonApiDotNetCoreExample.Data; -using Microsoft.EntityFrameworkCore; -using JsonApiDotNetCore.Extensions; -using System.Reflection; +using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCoreExample { @@ -15,20 +11,11 @@ public sealed class NoDefaultPageSizeStartup : Startup { public NoDefaultPageSizeStartup(IWebHostEnvironment env) : base(env) { } - public override void ConfigureServices(IServiceCollection services) + protected override void ConfigureJsonApiOptions(JsonApiOptions options) { - var mvcBuilder = services.AddMvcCore(); - services - .AddDbContext(options => options.UseNpgsql(GetDbConnectionString()), ServiceLifetime.Transient) - .AddJsonApi(options => { - options.Namespace = "api/v1"; - options.IncludeTotalRecordCount = true; - options.LoadDatabaseValues = true; - options.AllowClientGeneratedIds = true; - options.DefaultPageSize = 0; - }, - discovery => discovery.AddAssembly(Assembly.Load(nameof(JsonApiDotNetCoreExample))), - mvcBuilder: mvcBuilder); + base.ConfigureJsonApiOptions(options); + + options.DefaultPageSize = 0; } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index fb2ea4fd77..e79e31d018 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Extensions; using System; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Query; using JsonApiDotNetCoreExample.Services; @@ -37,20 +38,22 @@ public virtual void ConfigureServices(IServiceCollection services) .EnableSensitiveDataLogging() .UseNpgsql(GetDbConnectionString(), innerOptions => innerOptions.SetPostgresVersion(new Version(9,6))); }, ServiceLifetime.Transient) - .AddJsonApi(options => - { - options.IncludeExceptionStackTraceInErrors = true; - options.Namespace = "api/v1"; - options.DefaultPageSize = 5; - options.IncludeTotalRecordCount = true; - options.LoadDatabaseValues = true; - options.ValidateModelState = true; - }, - discovery => discovery.AddCurrentAssembly()); + .AddJsonApi(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly()); + // once all tests have been moved to WebApplicationFactory format we can get rid of this line below services.AddClientSerialization(); } + protected virtual void ConfigureJsonApiOptions(JsonApiOptions options) + { + options.IncludeExceptionStackTraceInErrors = true; + options.Namespace = "api/v1"; + options.DefaultPageSize = 5; + options.IncludeTotalRecordCount = true; + options.LoadDatabaseValues = true; + options.ValidateModelState = true; + } + public void Configure( IApplicationBuilder app, AppDbContext context) diff --git a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs index 897cc4e47f..be6885f8e3 100644 --- a/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/IResourceGraphBuilder.cs @@ -1,5 +1,4 @@ using System; -using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; @@ -19,7 +18,6 @@ public interface IResourceGraphBuilder /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. - /// See . /// IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; /// @@ -30,7 +28,6 @@ public interface IResourceGraphBuilder /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. - /// See . /// IResourceGraphBuilder AddResource(string pluralizedTypeName = null) where TResource : class, IIdentifiable; /// @@ -41,7 +38,6 @@ public interface IResourceGraphBuilder /// /// The pluralized name that should be exposed by the API. /// If nothing is specified, the configured name formatter will be used. - /// See . /// IResourceGraphBuilder AddResource(Type entityType, Type idType, string pluralizedTypeName = null); } diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index 8ff48140cc..cf5a01b8ce 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -30,7 +30,7 @@ namespace JsonApiDotNetCore.Builders /// public class JsonApiApplicationBuilder { - public readonly JsonApiOptions JsonApiOptions = new JsonApiOptions(); + private readonly JsonApiOptions _options = new JsonApiOptions(); internal IResourceGraphBuilder _resourceGraphBuilder; internal bool _usesDbContext; internal readonly IServiceCollection _services; @@ -46,13 +46,13 @@ public JsonApiApplicationBuilder(IServiceCollection services, IMvcCoreBuilder mv /// /// Executes the action provided by the user to configure /// - public void ConfigureJsonApiOptions(Action configureOptions) => configureOptions(JsonApiOptions); + public void ConfigureJsonApiOptions(Action configureOptions) => configureOptions(_options); /// - /// Configures built-in .net core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers need. + /// Configures built-in .NET Core MVC (things like middleware, routing). Most of this configuration can be adjusted for the developers' need. /// Before calling .AddJsonApi(), a developer can register their own implementation of the following services to customize startup: /// , , , - /// , and . + /// and . /// public void ConfigureMvc() { @@ -76,7 +76,7 @@ public void ConfigureMvc() options.Conventions.Insert(0, routingConvention); }); - if (JsonApiOptions.ValidateModelState) + if (_options.ValidateModelState) { _mvcBuilder.AddDataAnnotations(); } @@ -144,7 +144,7 @@ public void ConfigureServices() _services.AddScoped(typeof(IResourceQueryService<,>), typeof(DefaultResourceService<,>)); _services.AddScoped(typeof(IResourceCommandService<,>), typeof(DefaultResourceService<,>)); - _services.AddSingleton(JsonApiOptions); + _services.AddSingleton(_options); _services.AddSingleton(resourceGraph); _services.AddSingleton(); _services.AddSingleton(resourceGraph); @@ -165,7 +165,7 @@ public void ConfigureServices() AddServerSerialization(); AddQueryParameterServices(); - if (JsonApiOptions.EnableResourceHooks) + if (_options.EnableResourceHooks) AddResourceHooks(); _services.AddScoped(); @@ -214,8 +214,7 @@ private void AddServerSerialization() private void RegisterJsonApiStartupServices() { - _services.AddSingleton(JsonApiOptions); - _services.TryAddSingleton(new CamelCaseFormatter()); + _services.AddSingleton(_options); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(sp => new ServiceDiscoveryFacade(_services, sp.GetRequiredService())); diff --git a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs index e012f17c18..f661caf03c 100644 --- a/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs @@ -11,27 +11,26 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Links; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Builders { public class ResourceGraphBuilder : IResourceGraphBuilder { - private List Resources { get; } = new List(); - private List ValidationResults { get; } = new List(); - private IResourceNameFormatter Formatter { get; } = new CamelCaseFormatter(); + private readonly IJsonApiOptions _options; + private readonly List _resources = new List(); + private readonly List _validationResults = new List(); - public ResourceGraphBuilder() { } - - public ResourceGraphBuilder(IResourceNameFormatter formatter) + public ResourceGraphBuilder(IJsonApiOptions options) { - Formatter = formatter; + _options = options; } /// public IResourceGraph Build() { - Resources.ForEach(SetResourceLinksOptions); - var resourceGraph = new ResourceGraph(Resources, ValidationResults); + _resources.ForEach(SetResourceLinksOptions); + var resourceGraph = new ResourceGraph(_resources, _validationResults); return resourceGraph; } @@ -60,13 +59,13 @@ public IResourceGraphBuilder AddResource(Type resourceType, Type idType = null, AssertEntityIsNotAlreadyDefined(resourceType); if (resourceType.Implements()) { - pluralizedTypeName ??= Formatter.FormatResourceName(resourceType); + pluralizedTypeName ??= FormatResourceName(resourceType); idType ??= TypeLocator.GetIdType(resourceType); - Resources.Add(GetEntity(pluralizedTypeName, resourceType, idType)); + _resources.Add(GetEntity(pluralizedTypeName, resourceType, idType)); } else { - ValidationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. ")); + _validationResults.Add(new ValidationResult(LogLevel.Warning, $"{resourceType} does not implement 'IIdentifiable<>'. ")); } return this; @@ -98,7 +97,7 @@ protected virtual List GetAttributes(Type entityType) { var idAttr = new AttrAttribute { - PublicAttributeName = Formatter.FormatPropertyName(prop), + PublicAttributeName = FormatPropertyName(prop), PropertyInfo = prop }; attributes.Add(idAttr); @@ -109,7 +108,7 @@ protected virtual List GetAttributes(Type entityType) if (attribute == null) continue; - attribute.PublicAttributeName ??= Formatter.FormatPropertyName(prop); + attribute.PublicAttributeName ??= FormatPropertyName(prop); attribute.PropertyInfo = prop; attributes.Add(attribute); @@ -126,7 +125,7 @@ protected virtual List GetRelationships(Type entityType) var attribute = (RelationshipAttribute)prop.GetCustomAttribute(typeof(RelationshipAttribute)); if (attribute == null) continue; - attribute.PublicRelationshipName ??= Formatter.FormatPropertyName(prop); + attribute.PublicRelationshipName ??= FormatPropertyName(prop); attribute.InternalRelationshipName = prop.Name; attribute.RightType = GetRelationshipType(attribute, prop); attribute.LeftType = entityType; @@ -216,8 +215,20 @@ private static Type TypeOrElementType(Type type) private void AssertEntityIsNotAlreadyDefined(Type entityType) { - if (Resources.Any(e => e.ResourceType == entityType)) + if (_resources.Any(e => e.ResourceType == entityType)) throw new InvalidOperationException($"Cannot add entity type {entityType} to context resourceGraph, there is already an entity of that type configured."); } + + private string FormatResourceName(Type resourceType) + { + var formatter = new ResourceNameFormatter(_options); + return formatter.FormatResourceName(resourceType); + } + + private string FormatPropertyName(PropertyInfo resourceProperty) + { + var contractResolver = (DefaultContractResolver)_options.SerializerSettings.ContractResolver; + return contractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false); + } } } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index c50468eb5d..a416f45aba 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -45,6 +45,19 @@ public interface IJsonApiOptions : ILinksConfiguration /// bool AllowOmitDefaultQueryStringOverride { get; set; } + /// + /// Specifies the settings that are used by the . + /// Note that at some places a few settings are ignored, to ensure json:api spec compliance. + /// + /// The next example changes the casing convention to kebab casing. + /// + /// + /// JsonSerializerSettings SerializerSettings { get; } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index acf5121978..ab45db3f0b 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,6 +1,7 @@ using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Models.Links; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { @@ -123,6 +124,13 @@ public class JsonApiOptions : IJsonApiOptions /// public bool ValidateModelState { get; set; } - public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings(); + /// + public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + } + }; } } diff --git a/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs b/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs index 3629a783a2..f47c864a8e 100644 --- a/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs +++ b/src/JsonApiDotNetCore/Extensions/EntityFrameworkCoreExtension.cs @@ -41,7 +41,7 @@ public static IResourceGraphBuilder AddDbContext(this IResourceGraph private static string GetResourceNameFromDbSetProperty(PropertyInfo property, Type resourceType) { - // this check is actually duplicated in the DefaultResourceNameFormatter + // this check is actually duplicated in the ResourceNameFormatter // however, we perform it here so that we allow class attributes to be prioritized over // the DbSet attribute. Eventually, the DbSet attribute should be deprecated. // diff --git a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs index 9c224bbfa8..0aba5e5df5 100644 --- a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs @@ -1,4 +1,3 @@ -using JsonApiDotNetCore.Graph; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -6,31 +5,13 @@ namespace JsonApiDotNetCore.Extensions { internal static class JsonSerializerExtensions { - public static void ApplyErrorSettings(this JsonSerializer jsonSerializer, IResourceNameFormatter formatter) + public static void ApplyErrorSettings(this JsonSerializer jsonSerializer) { jsonSerializer.NullValueHandling = NullValueHandling.Ignore; - jsonSerializer.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new NewtonsoftNamingStrategyAdapter(formatter) - }; - } - - private sealed class NewtonsoftNamingStrategyAdapter : NamingStrategy - { - private readonly IResourceNameFormatter _formatter; - - public NewtonsoftNamingStrategyAdapter(IResourceNameFormatter formatter) - { - _formatter = formatter; - - ProcessDictionaryKeys = true; - ProcessExtensionDataNames = true; - } - protected override string ResolvePropertyName(string name) - { - return _formatter.ApplyCasingConvention(name); - } + var contractResolver = (DefaultContractResolver)jsonSerializer.ContractResolver; + contractResolver.NamingStrategy.ProcessDictionaryKeys = true; + contractResolver.NamingStrategy.ProcessExtensionDataNames = true; } } } diff --git a/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs b/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs index 4aa9f5b010..4cafa76366 100644 --- a/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs +++ b/src/JsonApiDotNetCore/Graph/ResourceIdMapper.cs @@ -11,7 +11,7 @@ public interface IRelatedIdMapper /// /// /// - /// DefaultResourceNameFormatter.FormatId("Article"); + /// DefaultRelatedIdMapper.GetRelatedIdPropertyName("Article"); /// // "ArticleId" /// /// diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs new file mode 100644 index 0000000000..f81be8dae3 --- /dev/null +++ b/src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs @@ -0,0 +1,42 @@ +using System; +using System.Reflection; +using Humanizer; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.Graph +{ + internal sealed class ResourceNameFormatter + { + private readonly NamingStrategy _namingStrategy; + + public ResourceNameFormatter(IJsonApiOptions options) + { + var contractResolver = (DefaultContractResolver) options.SerializerSettings.ContractResolver; + _namingStrategy = contractResolver.NamingStrategy; + } + + /// + /// Gets the publicly visible resource name from the internal type name. + /// + public string FormatResourceName(Type type) + { + try + { + // check the class definition first + // [Resource("models"] public class Model : Identifiable { /* ... */ } + if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + { + return attribute.ResourceName; + } + + return _namingStrategy.GetPropertyName(type.Name.Pluralize(), false); + } + catch (InvalidOperationException exception) + { + throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", exception); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs deleted file mode 100644 index 20b955cea9..0000000000 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/CamelCaseFormatter.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace JsonApiDotNetCore.Graph -{ - /// - /// Uses camelCase as formatting options in the route and request/response body. - /// - /// - /// - /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todoItems" - /// - /// - /// - /// Given the following property: - /// - /// public string CompoundProperty { get; set; } - /// - /// The public attribute will be formatted like so: - /// - /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compoundProperty" - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todoItems" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todoItem" - /// - /// - public sealed class CamelCaseFormatter: BaseResourceNameFormatter - { - /// - public override string ApplyCasingConvention(string properName) => char.ToLower(properName[0]) + properName.Substring(1); - } -} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs deleted file mode 100644 index 4795c33d31..0000000000 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/IResourceNameFormatter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Reflection; - -namespace JsonApiDotNetCore.Graph -{ - /// - /// Provides an interface for formatting resource names by convention - /// - public interface IResourceNameFormatter - { - /// - /// Get the publicly visible resource name from the internal type name - /// - string FormatResourceName(Type resourceType); - - /// - /// Get the publicly visible name for the given property - /// - string FormatPropertyName(PropertyInfo property); - - /// - /// Applies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - string ApplyCasingConvention(string properName); - } -} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs deleted file mode 100644 index 62baa3def6..0000000000 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/KebabCaseFormatter.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Text; - -namespace JsonApiDotNetCore.Graph -{ - /// - /// Uses kebab-case as formatting options in the route and request/response body. - /// - /// - /// - /// _default.FormatResourceName(typeof(TodoItem)).Dump(); - /// // > "todoItems" - /// - /// - /// - /// Given the following property: - /// - /// public string CompoundProperty { get; set; } - /// - /// The public attribute will be formatted like so: - /// - /// _default.FormatPropertyName(compoundProperty).Dump(); - /// // > "compound-property" - /// - /// - /// - /// - /// _default.ApplyCasingConvention("TodoItems"); - /// // > "todoItems" - /// - /// _default.ApplyCasingConvention("TodoItem"); - /// // > "todo-item" - /// - /// - public sealed class KebabCaseFormatter : BaseResourceNameFormatter - { - /// - public override string ApplyCasingConvention(string properName) - { - if (properName.Length == 0) - { - return properName; - } - - var chars = properName.ToCharArray(); - var builder = new StringBuilder(); - - for (var i = 0; i < chars.Length; i++) - { - if (char.IsUpper(chars[i])) - { - if (i > 0) - { - builder.Append('-'); - } - - builder.Append(char.ToLower(chars[i])); - } - else - { - builder.Append(chars[i]); - } - } - - return builder.ToString(); - } - } -} diff --git a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs b/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs deleted file mode 100644 index 1c092a912b..0000000000 --- a/src/JsonApiDotNetCore/Graph/ResourceNameFormatters/ResourceNameFormatterBase.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Reflection; -using Humanizer; -using JsonApiDotNetCore.Models; - -namespace JsonApiDotNetCore.Graph -{ - public abstract class BaseResourceNameFormatter : IResourceNameFormatter - { - /// - /// Uses the internal type name to determine the external resource name. - /// - public string FormatResourceName(Type type) - { - try - { - // check the class definition first - // [Resource("models"] public class Model : Identifiable { /* ... */ } - if (type.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) - return attribute.ResourceName; - - return ApplyCasingConvention(type.Name.Pluralize()); - } - catch (InvalidOperationException e) - { - throw new InvalidOperationException($"Cannot define multiple {nameof(ResourceAttribute)}s on type '{type}'.", e); - } - } - - /// - /// Applies the desired casing convention to the internal string. - /// This is generally applied to the type name after pluralization. - /// - public abstract string ApplyCasingConvention(string properName); - - /// - /// Uses the internal PropertyInfo to determine the external resource name. - /// By default the name will be formatted to camelCase. - /// - public string FormatPropertyName(PropertyInfo property) => ApplyCasingConvention(property.Name); - } -} diff --git a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs index 1e7ac181ab..e8f796f3c2 100644 --- a/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs @@ -1,6 +1,6 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Reflection; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; @@ -9,14 +9,16 @@ using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.Extensions.Options; +using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Internal { /// /// The default routing convention registers the name of the resource as the route - /// using the that is registered. The default for this is - /// a camelCase formatter. If the controller directly inherits from JsonApiMixin and there is no - /// resource directly associated, it used the name of the controller instead of the name of the type. + /// using the serializer casing convention. The default for this is + /// a camel case formatter. If the controller directly inherits from JsonApiMixin and there is no + /// resource directly associated, it uses the name of the controller instead of the name of the type. /// /// /// public class SomeResourceController: JsonApiController{SomeResource} { } @@ -35,14 +37,15 @@ namespace JsonApiDotNetCore.Internal /// public class DefaultRoutingConvention : IJsonApiRoutingConvention { - private readonly string _namespace; - private readonly IResourceNameFormatter _formatter; + private readonly IJsonApiOptions _options; + private readonly ResourceNameFormatter _formatter; private readonly HashSet _registeredTemplates = new HashSet(); private readonly Dictionary _registeredResources = new Dictionary(); - public DefaultRoutingConvention(IJsonApiOptions options, IResourceNameFormatter formatter) + + public DefaultRoutingConvention(IJsonApiOptions options) { - _namespace = options.Namespace; - _formatter = formatter; + _options = options; + _formatter = new ResourceNameFormatter(options); } /// @@ -90,7 +93,7 @@ private string TemplateFromResource(ControllerModel model) { if (_registeredResources.TryGetValue(model.ControllerName, out Type resourceType)) { - var template = $"{_namespace}/{_formatter.FormatResourceName(resourceType)}"; + var template = $"{_options.Namespace}/{_formatter.FormatResourceName(resourceType)}"; if (_registeredTemplates.Add(template)) { return template; @@ -104,7 +107,10 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - var template = $"{_namespace}/{_formatter.ApplyCasingConvention(model.ControllerName)}"; + var contractResolver = (DefaultContractResolver) _options.SerializerSettings.ContractResolver; + string controllerName = contractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false); + + var template = $"{_options.Namespace}/{controllerName}"; if (_registeredTemplates.Add(template)) { return template; diff --git a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs index 48b152eb45..9b6f885b15 100644 --- a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers.Contracts; @@ -29,7 +28,6 @@ public sealed class CurrentRequestMiddleware private IJsonApiOptions _options; private RouteValueDictionary _routeValues; private IControllerResourceMapping _controllerResourceMapping; - private IResourceNameFormatter _formatter; public CurrentRequestMiddleware(RequestDelegate next) { @@ -40,15 +38,13 @@ public async Task Invoke(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, ICurrentRequest currentRequest, - IResourceGraph resourceGraph, - IResourceNameFormatter formatter) + IResourceGraph resourceGraph) { _httpContext = httpContext; _currentRequest = currentRequest; _controllerResourceMapping = controllerResourceMapping; _resourceGraph = resourceGraph; _options = options; - _formatter = formatter; _routeValues = httpContext.GetRouteData().Values; var requestResource = GetCurrentEntity(); if (requestResource != null) @@ -204,7 +200,7 @@ private async Task FlushResponseAsync(HttpContext context, Error error) context.Response.StatusCode = (int) error.StatusCode; JsonSerializer serializer = JsonSerializer.CreateDefault(_options.SerializerSettings); - serializer.ApplyErrorSettings(_formatter); + serializer.ApplyErrorSettings(); // https://github.com/JamesNK/Newtonsoft.Json/issues/1193 await using (var stream = new MemoryStream()) diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs index 055bc8c72c..66d4f77111 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -31,7 +31,6 @@ public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerial private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); private readonly IFieldsToSerialize _fieldsToSerialize; - private readonly IResourceNameFormatter _formatter; private readonly IJsonApiOptions _options; private readonly IMetaBuilder _metaBuilder; private readonly Type _primaryResourceType; @@ -43,12 +42,10 @@ public ResponseSerializer(IMetaBuilder metaBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, - IResourceNameFormatter formatter, IJsonApiOptions options) : base(resourceObjectBuilder) { _fieldsToSerialize = fieldsToSerialize; - _formatter = formatter; _options = options; _linkBuilder = linkBuilder; _metaBuilder = metaBuilder; @@ -68,7 +65,7 @@ public string Serialize(object data) private string SerializeErrorDocument(ErrorDocument errorDocument) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => { serializer.ApplyErrorSettings(_formatter); }); + return SerializeObject(errorDocument, _options.SerializerSettings, serializer => { serializer.ApplyErrorSettings(); }); } /// diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 9a62c99457..244cc76e60 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -25,14 +25,17 @@ namespace DiscoveryTests public sealed class ServiceDiscoveryFacadeTests { private readonly IServiceCollection _services = new ServiceCollection(); - private readonly ResourceGraphBuilder _resourceGraphBuilder = new ResourceGraphBuilder(); + private readonly ResourceGraphBuilder _resourceGraphBuilder; public ServiceDiscoveryFacadeTests() { + var options = new JsonApiOptions(); + var dbResolverMock = new Mock(); dbResolverMock.Setup(m => m.GetContext()).Returns(new Mock().Object); TestModelRepository._dbContextResolver = dbResolverMock.Object; - _services.AddSingleton(new JsonApiOptions()); + + _services.AddSingleton(options); _services.AddSingleton(new LoggerFactory()); _services.AddScoped((_) => new Mock().Object); _services.AddScoped((_) => new Mock().Object); @@ -40,6 +43,8 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped((_) => new Mock().Object); _services.AddScoped((_) => new Mock().Object); _services.AddScoped((_) => new Mock().Object); + + _resourceGraphBuilder = new ResourceGraphBuilder(options); } private ServiceDiscoveryFacade Facade => new ServiceDiscoveryFacade(_services, _resourceGraphBuilder); diff --git a/test/IntegrationTests/Data/EntityRepositoryTests.cs b/test/IntegrationTests/Data/EntityRepositoryTests.cs index 033cd3a399..d4955ac42f 100644 --- a/test/IntegrationTests/Data/EntityRepositoryTests.cs +++ b/test/IntegrationTests/Data/EntityRepositoryTests.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -153,7 +154,7 @@ public async Task Paging_PageNumberIsNegative_GiveBackReverseAmountOfIds(int pag { var contextResolverMock = new Mock(); contextResolverMock.Setup(m => m.GetContext()).Returns(context); - var resourceGraph = new ResourceGraphBuilder().AddResource().Build(); + var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build(); var targetedFields = new Mock(); var repository = new DefaultResourceRepository(targetedFields.Object, contextResolverMock.Object, resourceGraph, null, NullLoggerFactory.Instance); return (repository, targetedFields); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 4a2dd5a3ed..79ab714435 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -3,6 +3,7 @@ using System.Net; using System.Threading.Tasks; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCoreExample; @@ -39,7 +40,9 @@ public async Task Can_Include_Nested_Relationships() { // Arrange const string route = "/api/v1/todoItems?include=collection.owner"; - var resourceGraph = new ResourceGraphBuilder().AddResource("todoItems").AddResource().AddResource().Build(); + + var options = _fixture.GetService(); + var resourceGraph = new ResourceGraphBuilder(options).AddResource("todoItems").AddResource().AddResource().Build(); var deserializer = new ResponseDeserializer(resourceGraph); var todoItem = new TodoItem { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index 99286f3d64..70c59edb55 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -5,6 +5,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; @@ -73,9 +74,10 @@ protected IRequestSerializer GetSerializer(Expression(); + var options = GetService(); + var formatter = new ResourceNameFormatter(options); var resourcesContexts = GetService().GetResourceContexts(); - var builder = new ResourceGraphBuilder(formatter); + var builder = new ResourceGraphBuilder(options); foreach (var rc in resourcesContexts) { if (rc.ResourceType == typeof(TodoItem) || rc.ResourceType == typeof(TodoItemCollection) || rc.ResourceType == typeof(Passport)) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 7da5372c3d..6c9629d59a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -17,6 +17,7 @@ using System.Net; using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCoreExampleTests.Helpers.Models; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models.JsonApiDocuments; @@ -26,6 +27,7 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec [Collection("WebHostCollection")] public sealed class SparseFieldSetTests { + private readonly TestFixture _fixture; private readonly AppDbContext _dbContext; private readonly IResourceGraph _resourceGraph; private readonly Faker _personFaker; @@ -33,6 +35,7 @@ public sealed class SparseFieldSetTests public SparseFieldSetTests(TestFixture fixture) { + _fixture = fixture; _dbContext = fixture.GetService(); _resourceGraph = fixture.GetService(); _personFaker = new Faker() @@ -169,7 +172,8 @@ public async Task Fields_Query_Selects_All_Fieldset_With_HasOne() var route = "/api/v1/todoItems?include=owner&fields[owner]=firstName,age"; var request = new HttpRequestMessage(httpMethod, route); - var resourceGraph = new ResourceGraphBuilder().AddResource().AddResource("todoItems").Build(); + var options = _fixture.GetService(); + var resourceGraph = new ResourceGraphBuilder(options).AddResource().AddResource("todoItems").Build(); var deserializer = new ResponseDeserializer(resourceGraph); // Act var response = await client.SendAsync(request); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index 4c4b75e3b6..c13278ae3c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -9,6 +9,7 @@ using System.Linq.Expressions; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCoreExampleTests.Helpers.Models; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCore.Internal.Contracts; @@ -44,9 +45,12 @@ public IRequestSerializer GetSerializer(Expression(); + + var resourceGraph = new ResourceGraphBuilder(options) .AddResource() .AddResource
() .AddResource() diff --git a/test/JsonApiDotNetCoreExampleTests/Factories/KebabCaseApplicationFactory.cs b/test/JsonApiDotNetCoreExampleTests/Factories/KebabCaseApplicationFactory.cs index a51dfde0b0..2094e2a89b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Factories/KebabCaseApplicationFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/Factories/KebabCaseApplicationFactory.cs @@ -1,7 +1,5 @@ -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Graph; +using JsonApiDotNetCoreExample; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; namespace JsonApiDotNetCoreExampleTests { @@ -9,11 +7,7 @@ public class KebabCaseApplicationFactory : CustomApplicationFactoryBase { protected override void ConfigureWebHost(IWebHostBuilder builder) { - builder.ConfigureServices(services => - { - services.AddSingleton(); - services.AddClientSerialization(); - }); + builder.UseStartup(); } } } diff --git a/test/NoEntityFrameworkTests/TestFixture.cs b/test/NoEntityFrameworkTests/TestFixture.cs index 5196a1d7a8..fc6d6bc0df 100644 --- a/test/NoEntityFrameworkTests/TestFixture.cs +++ b/test/NoEntityFrameworkTests/TestFixture.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization.Client; @@ -8,6 +8,7 @@ using NoEntityFrameworkExample.Models; using System; using System.Linq.Expressions; +using JsonApiDotNetCore.Configuration; using Startup = NoEntityFrameworkExample.Startup; namespace NoEntityFrameworkTests @@ -39,7 +40,9 @@ public IRequestSerializer GetSerializer(Expression("todoItems").Build(); + var options = GetService(); + + var resourceGraph = new ResourceGraphBuilder(options).AddResource("todoItems").Build(); return new ResponseDeserializer(resourceGraph); } diff --git a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs index fc66d36f2f..5d2d53f116 100644 --- a/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs +++ b/test/UnitTests/Builders/ContextGraphBuilder_Tests.cs @@ -4,6 +4,7 @@ using System.Reflection; using Humanizer; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions.EntityFrameworkCore; using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Internal.Contracts; @@ -44,26 +45,11 @@ public void Can_Build_ResourceGraph_Using_Builder() Assert.Equal(typeof(ResourceDefinition), nonDbResource.ResourceDefinitionType); } - [Fact] - public void Resources_Without_Names_Specified_Will_Use_Default_Formatter() - { - // Arrange - var builder = new ResourceGraphBuilder(); - builder.AddResource(); - - // Act - var resourceGraph = builder.Build(); - - // Assert - var resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("testResources", resource.ResourceName); - } - [Fact] public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() { // Arrange - var builder = new ResourceGraphBuilder(new CamelCaseFormatter()); + var builder = new ResourceGraphBuilder(new JsonApiOptions()); builder.AddResource(); // Act @@ -74,26 +60,11 @@ public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() Assert.Equal("testResources", resource.ResourceName); } - [Fact] - public void Attrs_Without_Names_Specified_Will_Use_Default_Formatter() - { - // Arrange - var builder = new ResourceGraphBuilder(); - builder.AddResource(); - - // Act - var resourceGraph = builder.Build(); - - // Assert - var resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Contains(resource.Attributes, (i) => i.PublicAttributeName == "compoundAttribute"); - } - [Fact] public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() { // Arrange - var builder = new ResourceGraphBuilder(new CamelCaseFormatter()); + var builder = new ResourceGraphBuilder(new JsonApiOptions()); builder.AddResource(); // Act @@ -105,10 +76,10 @@ public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() } [Fact] - public void Relationships_Without_Names_Specified_Will_Use_Default_Formatter() + public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter() { // Arrange - var builder = new ResourceGraphBuilder(); + var builder = new ResourceGraphBuilder(new JsonApiOptions()); builder.AddResource(); // Act diff --git a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs index ce6d4bf742..20695c89c5 100644 --- a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs +++ b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs @@ -1,3 +1,5 @@ +// TODO: Why is this file commented out? + //using JsonApiDotNetCore.Builders; //using JsonApiDotNetCore.Configuration; //using JsonApiDotNetCore.Services; diff --git a/test/UnitTests/Builders/MetaBuilderTests.cs b/test/UnitTests/Builders/MetaBuilderTests.cs index 58e35b7bd6..a691f2d387 100644 --- a/test/UnitTests/Builders/MetaBuilderTests.cs +++ b/test/UnitTests/Builders/MetaBuilderTests.cs @@ -1,3 +1,5 @@ +// TODO: Why is this file commented out? + //using System.Collections.Generic; //using JsonApiDotNetCore.Builders; //using Xunit; diff --git a/test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs b/test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs index e6475f242d..9646a4ebd7 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilder_Tests.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions.EntityFrameworkCore; using JsonApiDotNetCore.Internal; using Microsoft.EntityFrameworkCore; @@ -13,7 +14,7 @@ public sealed class ResourceGraphBuilder_Tests public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_DoNot_Implement_IIdentifiable() { // Arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); + var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions()); // Act resourceGraphBuilder.AddDbContext(); @@ -27,7 +28,7 @@ public void AddDbContext_Does_Not_Throw_If_Context_Contains_Members_That_DoNot_I public void Adding_DbContext_Members_That_DoNot_Implement_IIdentifiable_Creates_Warning() { // Arrange - var resourceGraphBuilder = new ResourceGraphBuilder(); + var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions()); // Act resourceGraphBuilder.AddDbContext(); diff --git a/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs b/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs index 19dfdbcef7..73c368215f 100644 --- a/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs +++ b/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs @@ -11,7 +11,6 @@ using System.IO; using System.Linq; using System.Threading.Tasks; -using JsonApiDotNetCore.Graph; using Xunit; namespace UnitTests.Middleware @@ -88,7 +87,7 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) var options = holder.Options.Object; var currentRequest = holder.CurrentRequest; var resourceGraph = holder.ResourceGraph.Object; - return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, currentRequest, resourceGraph, new CamelCaseFormatter()); + return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, currentRequest, resourceGraph); } private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", string action = "", string id =null, Type relType = null) { diff --git a/test/UnitTests/Models/ConstructionTests.cs b/test/UnitTests/Models/ConstructionTests.cs index 401472e491..45d25d1832 100644 --- a/test/UnitTests/Models/ConstructionTests.cs +++ b/test/UnitTests/Models/ConstructionTests.cs @@ -1,5 +1,6 @@ using System; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Server; @@ -13,7 +14,7 @@ public sealed class ConstructionTests public void When_model_has_no_parameterless_contructor_it_must_fail() { // Arrange - var graph = new ResourceGraphBuilder().AddResource().Build(); + var graph = new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build(); var serializer = new RequestDeserializer(graph, new TargetedFields()); diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index 67f7fdf40f..1e2a2ebbe1 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -2,6 +2,7 @@ using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; using System.Linq; +using JsonApiDotNetCore.Configuration; using Xunit; namespace UnitTests.Models @@ -47,7 +48,7 @@ public sealed class RequestFilteredResource : ResourceDefinition { // this constructor will be resolved from the container // that means you can take on any dependency that is also defined in the container - public RequestFilteredResource(bool isAdmin) : base(new ResourceGraphBuilder().AddResource().Build()) + public RequestFilteredResource(bool isAdmin) : base(new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build()) { if (isAdmin) HideFields(m => m.AlwaysExcluded); diff --git a/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs b/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs index 24136f87d8..1b4688ee4a 100644 --- a/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs +++ b/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs @@ -1,5 +1,6 @@ -using System; +using System; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers.Contracts; @@ -17,7 +18,7 @@ public class QueryParametersUnitTestCollection public QueryParametersUnitTestCollection() { - var builder = new ResourceGraphBuilder(); + var builder = new ResourceGraphBuilder(new JsonApiOptions()); builder.AddResource
(); builder.AddResource(); builder.AddResource(); @@ -47,4 +48,4 @@ public IResourceDefinitionProvider MockResourceDefinitionProvider(params (Type, return mock.Object; } } -} \ No newline at end of file +} diff --git a/test/UnitTests/ResourceHooks/DiscoveryTests.cs b/test/UnitTests/ResourceHooks/DiscoveryTests.cs index de8319c314..bf40e0733a 100644 --- a/test/UnitTests/ResourceHooks/DiscoveryTests.cs +++ b/test/UnitTests/ResourceHooks/DiscoveryTests.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using System; +using JsonApiDotNetCore.Configuration; using Microsoft.Extensions.DependencyInjection; namespace UnitTests.ResourceHooks @@ -15,7 +16,7 @@ public sealed class DiscoveryTests public class Dummy : Identifiable { } public sealed class DummyResourceDefinition : ResourceDefinition { - public DummyResourceDefinition() : base(new ResourceGraphBuilder().AddResource().Build()) { } + public DummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build()) { } public override IEnumerable BeforeDelete(IEntityHashSet affected, ResourcePipeline pipeline) { return affected; } public override void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } @@ -48,7 +49,7 @@ public override void AfterDelete(HashSet entities, ResourcePipeline pipeline, public sealed class AnotherDummyResourceDefinition : ResourceDefinitionBase { - public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder().AddResource().Build()) { } + public AnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build()) { } } [Fact] @@ -64,7 +65,7 @@ public void HookDiscovery_InheritanceSubclass_CanDiscover() public class YetAnotherDummy : Identifiable { } public sealed class YetAnotherDummyResourceDefinition : ResourceDefinition { - public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder().AddResource().Build()) { } + public YetAnotherDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build()) { } public override IEnumerable BeforeDelete(IEntityHashSet affected, ResourcePipeline pipeline) { return affected; } @@ -96,7 +97,7 @@ public void HookDiscovery_InheritanceWithGenericSubclass_CanDiscover() public sealed class GenericDummyResourceDefinition : ResourceDefinition where TResource : class, IIdentifiable { - public GenericDummyResourceDefinition() : base(new ResourceGraphBuilder().AddResource().Build()) { } + public GenericDummyResourceDefinition() : base(new ResourceGraphBuilder(new JsonApiOptions()).AddResource().Build()) { } public override IEnumerable BeforeDelete(IEntityHashSet entities, ResourcePipeline pipeline) { return entities; } public override void AfterDelete(HashSet entities, ResourcePipeline pipeline, bool succeeded) { } diff --git a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs index c200a2dd58..234cb8af5d 100644 --- a/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs +++ b/test/UnitTests/ResourceHooks/ResourceHooksTestsSetup.cs @@ -35,9 +35,10 @@ public class HooksDummyData protected readonly Faker _articleTagFaker; protected readonly Faker _identifiableArticleTagFaker; protected readonly Faker _passportFaker; + public HooksDummyData() { - _resourceGraph = new ResourceGraphBuilder() + _resourceGraph = new ResourceGraphBuilder(new JsonApiOptions()) .AddResource() .AddResource() .AddResource() @@ -186,7 +187,7 @@ public class HooksTestsSetup : HooksDummyData var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; - var resourceGraph = new ResourceGraphBuilder() + var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions()) .AddResource() .AddResource() .Build(); @@ -222,7 +223,7 @@ public class HooksTestsSetup : HooksDummyData var dbContext = repoDbContextOptions != null ? new AppDbContext(repoDbContextOptions) : null; - var resourceGraph = new ResourceGraphBuilder() + var resourceGraph = new ResourceGraphBuilder(new JsonApiOptions()) .AddResource() .AddResource() .AddResource() diff --git a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs index 81ea58bb92..610fb983a1 100644 --- a/test/UnitTests/Serialization/SerializationTestsSetupBase.cs +++ b/test/UnitTests/Serialization/SerializationTestsSetupBase.cs @@ -1,5 +1,6 @@ -using Bogus; +using Bogus; using JsonApiDotNetCore.Builders; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal.Contracts; using UnitTests.TestModels; using Person = UnitTests.TestModels.Person; @@ -37,7 +38,7 @@ public SerializationTestsSetupBase() protected IResourceGraph BuildGraph() { - var resourceGraphBuilder = new ResourceGraphBuilder(); + var resourceGraphBuilder = new ResourceGraphBuilder(new JsonApiOptions()); resourceGraphBuilder.AddResource("testResource"); resourceGraphBuilder.AddResource("testResource-with-list"); // one to one relationships @@ -61,4 +62,4 @@ protected IResourceGraph BuildGraph() return resourceGraphBuilder.Build(); } } -} \ No newline at end of file +} diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index d98de0e4d5..8bfc24e0fa 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -47,7 +47,7 @@ protected ResponseSerializer GetResponseSerializer(List(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, new CamelCaseFormatter(), new JsonApiOptions()); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, new JsonApiOptions()); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(List> inclusionChains = null, ResourceLinks resourceLinks = null, RelationshipLinks relationshipLinks = null) diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 7f0cedffd6..5d46805d18 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -35,11 +35,10 @@ public EntityResourceService_Tests() _pageService = new Mock(); _sortService = new Mock(); _filterService = new Mock(); - _resourceGraph = new ResourceGraphBuilder() + _resourceGraph = new ResourceGraphBuilder(new JsonApiOptions()) .AddResource() .AddResource() .Build(); - } [Fact] From f236cd5041f2c979e7be86e5368802d861662722 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 9 Apr 2020 18:27:04 +0200 Subject: [PATCH 05/11] Added explicit attribute name to ensure that value is respected in input/output --- src/Examples/JsonApiDotNetCoreExample/Models/Person.cs | 2 +- .../Acceptance/Spec/SparseFieldSetTests.cs | 6 +++--- .../Acceptance/TodoItemsControllerTests.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 58e2c8c67b..386c2029ba 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -20,7 +20,7 @@ public sealed class Person : Identifiable, IIsLockable [Attr] public string LastName { get; set; } - [Attr] + [Attr("the-Age")] public int Age { get; set; } [HasMany] diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 6c9629d59a..2fe4565bd1 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -170,7 +170,7 @@ public async Task Fields_Query_Selects_All_Fieldset_With_HasOne() using var server = new TestServer(builder); var client = server.CreateClient(); - var route = "/api/v1/todoItems?include=owner&fields[owner]=firstName,age"; + var route = "/api/v1/todoItems?include=owner&fields[owner]=firstName,the-Age"; var request = new HttpRequestMessage(httpMethod, route); var options = _fixture.GetService(); var resourceGraph = new ResourceGraphBuilder(options).AddResource().AddResource("todoItems").Build(); @@ -213,7 +213,7 @@ public async Task Fields_Query_Selects_Fieldset_With_HasOne() using var server = new TestServer(builder); var client = server.CreateClient(); - var route = $"/api/v1/todoItems/{todoItem.Id}?include=owner&fields[owner]=firstName,age"; + var route = $"/api/v1/todoItems/{todoItem.Id}?include=owner&fields[owner]=firstName,the-Age"; var request = new HttpRequestMessage(httpMethod, route); // Act @@ -228,7 +228,7 @@ public async Task Fields_Query_Selects_Fieldset_With_HasOne() var included = deserializeBody.Included.First(); Assert.Equal(owner.StringId, included.Id); Assert.Equal(owner.FirstName, included.Attributes["firstName"]); - Assert.Equal((long)owner.Age, included.Attributes["age"]); + Assert.Equal((long)owner.Age, included.Attributes["the-Age"]); Assert.DoesNotContain("lastName", included.Attributes.Keys); } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 2825371b7a..7a5ee9b249 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -354,7 +354,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Ascending() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todoItems?page[size]={numberOfItems}&include=owner&sort=owner.age"; + var route = $"/api/v1/todoItems?page[size]={numberOfItems}&include=owner&sort=owner.the-Age"; var request = new HttpRequestMessage(httpMethod, route); // Act @@ -392,7 +392,7 @@ public async Task Can_Sort_TodoItems_By_Nested_Attribute_Descending() _context.SaveChanges(); var httpMethod = new HttpMethod("GET"); - var route = $"/api/v1/todoItems?page[size]={numberOfItems}&include=owner&sort=-owner.age"; + var route = $"/api/v1/todoItems?page[size]={numberOfItems}&include=owner&sort=-owner.the-Age"; var request = new HttpRequestMessage(httpMethod, route); // Act From d5968fc74e92af50eb429072ca81881b41a62ba4 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 10 Apr 2020 13:42:29 +0200 Subject: [PATCH 06/11] Replaced 'omitNull' and 'omitDefault' query string parameters with 'nulls' and 'defaults'. Making them a single word allows us to not take the casing convention into account. Note this inverts the meaning of true/false in the query string value. --- benchmarks/Query/QueryParserBenchmarks.cs | 8 +-- .../Builders/JsonApiApplicationBuilder.cs | 8 +-- .../DefaultAttributeResponseBehavior.cs | 26 --------- .../Configuration/IJsonApiOptions.cs | 8 +-- .../Configuration/JsonApiOptions.cs | 4 +- .../NullAttributeResponseBehavior.cs | 26 --------- .../StandardQueryStringParameters.cs | 8 +-- ...tDefaultService.cs => IDefaultsService.cs} | 4 +- .../{IOmitNullService.cs => INullsService.cs} | 4 +- ...itDefaultService.cs => DefaultsService.cs} | 14 ++--- .../{OmitNullService.cs => NullsService.cs} | 14 ++--- .../ResourceObjectBuilderSettingsProvider.cs | 14 ++--- .../OmitAttributeIfValueIsNullTests.cs | 56 ++++++++++--------- ...erviceTests.cs => DefaultsServiceTests.cs} | 36 ++++++------ ...llServiceTests.cs => NullsServiceTests.cs} | 36 ++++++------ 15 files changed, 108 insertions(+), 158 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs delete mode 100644 src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs rename src/JsonApiDotNetCore/QueryParameterServices/Contracts/{IOmitDefaultService.cs => IDefaultsService.cs} (80%) rename src/JsonApiDotNetCore/QueryParameterServices/Contracts/{IOmitNullService.cs => INullsService.cs} (80%) rename src/JsonApiDotNetCore/QueryParameterServices/{OmitDefaultService.cs => DefaultsService.cs} (73%) rename src/JsonApiDotNetCore/QueryParameterServices/{OmitNullService.cs => NullsService.cs} (74%) rename test/UnitTests/QueryParameters/{OmitDefaultServiceTests.cs => DefaultsServiceTests.cs} (64%) rename test/UnitTests/QueryParameters/{OmitNullServiceTests.cs => NullsServiceTests.cs} (65%) diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index ba8050717b..79b9caabf1 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -57,13 +57,13 @@ private static QueryParameterParser CreateQueryParameterDiscoveryForAll(IResourc ISortService sortService = new SortService(resourceDefinitionProvider, resourceGraph, currentRequest); ISparseFieldsService sparseFieldsService = new SparseFieldsService(resourceGraph, currentRequest); IPageService pageService = new PageService(options, resourceGraph, currentRequest); - IOmitDefaultService omitDefaultService = new OmitDefaultService(options); - IOmitNullService omitNullService = new OmitNullService(options); + IDefaultsService defaultsService = new DefaultsService(options); + INullsService nullsService = new NullsService(options); var queryServices = new List { - includeService, filterService, sortService, sparseFieldsService, pageService, omitDefaultService, - omitNullService + includeService, filterService, sortService, sparseFieldsService, pageService, defaultsService, + nullsService }; return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance); diff --git a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs index cf5a01b8ce..8cbe4eba4a 100644 --- a/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs @@ -178,16 +178,16 @@ private void AddQueryParameterServices() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); + _services.AddScoped(); + _services.AddScoped(); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); - _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); + _services.AddScoped(sp => sp.GetService()); } private void AddResourceHooks() diff --git a/src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs b/src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs deleted file mode 100644 index dcb9a34e71..0000000000 --- a/src/JsonApiDotNetCore/Configuration/DefaultAttributeResponseBehavior.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace JsonApiDotNetCore.Configuration -{ - /// - /// Determines how attributes that contain a default value are serialized in the response payload. - /// - public struct DefaultAttributeResponseBehavior - { - /// Determines whether to serialize attributes that contain their types' default value. - /// Determines whether serialization behavior can be controlled by a query string parameter. - public DefaultAttributeResponseBehavior(bool omitAttributeIfValueIsDefault = false, bool allowQueryStringOverride = false) - { - OmitAttributeIfValueIsDefault = omitAttributeIfValueIsDefault; - AllowQueryStringOverride = allowQueryStringOverride; - } - - /// - /// Determines whether to serialize attributes that contain their types' default value. - /// - public bool OmitAttributeIfValueIsDefault { get; } - - /// - /// Determines whether serialization behavior can be controlled by a query string parameter. - /// - public bool AllowQueryStringOverride { get; } - } -} diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index a416f45aba..cf886b6730 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -36,14 +36,14 @@ public interface IJsonApiOptions : ILinksConfiguration string Namespace { get; set; } /// - /// Determines whether the serialization setting can be overridden by a query string parameter. + /// Determines whether the serialization setting can be overridden by using a query string parameter. /// - bool AllowOmitNullQueryStringOverride { get; set; } + bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } /// - /// Determines whether the serialization setting can be overridden by a query string parameter. + /// Determines whether the serialization setting can be overridden by using a query string parameter. /// - bool AllowOmitDefaultQueryStringOverride { get; set; } + bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; } /// /// Specifies the settings that are used by the . diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index ab45db3f0b..34e44e97d9 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -54,10 +54,10 @@ public class JsonApiOptions : IJsonApiOptions public string Namespace { get; set; } /// - public bool AllowOmitNullQueryStringOverride { get; set; } + public bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } /// - public bool AllowOmitDefaultQueryStringOverride { get; set; } + public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; } /// /// The default page size for all resources. The value zero means: no paging. diff --git a/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs b/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs deleted file mode 100644 index c1b1e7c37e..0000000000 --- a/src/JsonApiDotNetCore/Configuration/NullAttributeResponseBehavior.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace JsonApiDotNetCore.Configuration -{ - /// - /// Determines how attributes that contain null are serialized in the response payload. - /// - public struct NullAttributeResponseBehavior - { - /// Determines whether to serialize attributes that contain null. - /// Determines whether serialization behavior can be controlled by a query string parameter. - public NullAttributeResponseBehavior(bool omitAttributeIfValueIsNull = false, bool allowQueryStringOverride = false) - { - OmitAttributeIfValueIsNull = omitAttributeIfValueIsNull; - AllowQueryStringOverride = allowQueryStringOverride; - } - - /// - /// Determines whether to serialize attributes that contain null. - /// - public bool OmitAttributeIfValueIsNull { get; } - - /// - /// Determines whether serialization behavior can be controlled by a query string parameter. - /// - public bool AllowQueryStringOverride { get; } - } -} diff --git a/src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs b/src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs index 9bd8d1d673..766b564d54 100644 --- a/src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs +++ b/src/JsonApiDotNetCore/Controllers/StandardQueryStringParameters.cs @@ -11,10 +11,8 @@ public enum StandardQueryStringParameters Include = 4, Page = 8, Fields = 16, - // TODO: Rename to single-word to prevent violating casing conventions. - OmitNull = 32, - // TODO: Rename to single-word to prevent violating casing conventions. - OmitDefault = 64, - All = Filter | Sort | Include | Page | Fields | OmitNull | OmitDefault + Nulls = 32, + Defaults = 64, + All = Filter | Sort | Include | Page | Fields | Nulls | Defaults } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs similarity index 80% rename from src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs index ec8213fdb7..842f245db4 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs @@ -1,9 +1,9 @@ namespace JsonApiDotNetCore.Query { /// - /// Query parameter service responsible for url queries of the form ?omitDefault=true + /// Query parameter service responsible for url queries of the form ?defaults=false /// - public interface IOmitDefaultService : IQueryParameterService + public interface IDefaultsService : IQueryParameterService { /// /// Contains the effective value of default configuration and query string override, after parsing has occured. diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs similarity index 80% rename from src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs index 1b21ee8b37..1a24070b02 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs @@ -1,9 +1,9 @@ namespace JsonApiDotNetCore.Query { /// - /// Query parameter service responsible for url queries of the form ?omitNull=true + /// Query parameter service responsible for url queries of the form ?nulls=false /// - public interface IOmitNullService : IQueryParameterService + public interface INullsService : IQueryParameterService { /// /// Contains the effective value of default configuration and query string override, after parsing has occured. diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs similarity index 73% rename from src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs index 13061aa066..0062360713 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs @@ -7,11 +7,11 @@ namespace JsonApiDotNetCore.Query { /// - public class OmitDefaultService : QueryParameterService, IOmitDefaultService + public class DefaultsService : QueryParameterService, IDefaultsService { private readonly IJsonApiOptions _options; - public OmitDefaultService(IJsonApiOptions options) + public DefaultsService(IJsonApiOptions options) { OmitAttributeIfValueIsDefault = options.SerializerSettings.DefaultValueHandling == DefaultValueHandling.Ignore; _options = options; @@ -22,27 +22,27 @@ public OmitDefaultService(IJsonApiOptions options) public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) { - return _options.AllowOmitDefaultQueryStringOverride && - !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.OmitDefault); + return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && + !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Defaults); } /// public bool CanParse(string parameterName) { - return parameterName == "omitDefault"; + return parameterName == "defaults"; } /// public virtual void Parse(string parameterName, StringValues parameterValue) { - if (!bool.TryParse(parameterValue, out var omitAttributeIfValueIsDefault)) + if (!bool.TryParse(parameterValue, out var result)) { throw new InvalidQueryStringParameterException(parameterName, "The specified query string value must be 'true' or 'false'.", $"The value '{parameterValue}' for parameter '{parameterName}' is not a valid boolean value."); } - OmitAttributeIfValueIsDefault = omitAttributeIfValueIsDefault; + OmitAttributeIfValueIsDefault = !result; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs similarity index 74% rename from src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs index a9008ba077..6e71b0bdb1 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs @@ -7,11 +7,11 @@ namespace JsonApiDotNetCore.Query { /// - public class OmitNullService : QueryParameterService, IOmitNullService + public class NullsService : QueryParameterService, INullsService { private readonly IJsonApiOptions _options; - public OmitNullService(IJsonApiOptions options) + public NullsService(IJsonApiOptions options) { OmitAttributeIfValueIsNull = options.SerializerSettings.NullValueHandling == NullValueHandling.Ignore; _options = options; @@ -23,27 +23,27 @@ public OmitNullService(IJsonApiOptions options) /// public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) { - return _options.AllowOmitNullQueryStringOverride && - !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.OmitNull); + return _options.AllowQueryStringOverrideForSerializerNullValueHandling && + !disableQueryAttribute.ContainsParameter(StandardQueryStringParameters.Nulls); } /// public bool CanParse(string parameterName) { - return parameterName == "omitNull"; + return parameterName == "nulls"; } /// public virtual void Parse(string parameterName, StringValues parameterValue) { - if (!bool.TryParse(parameterValue, out var omitAttributeIfValueIsNull)) + if (!bool.TryParse(parameterValue, out var result)) { throw new InvalidQueryStringParameterException(parameterName, "The specified query string value must be 'true' or 'false'.", $"The value '{parameterValue}' for parameter '{parameterName}' is not a valid boolean value."); } - OmitAttributeIfValueIsNull = omitAttributeIfValueIsNull; + OmitAttributeIfValueIsNull = !result; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs index d75e7bde98..0acece0d30 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs @@ -9,20 +9,20 @@ namespace JsonApiDotNetCore.Serialization.Server /// public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider { - private readonly IOmitDefaultService _defaultAttributeValues; - private readonly IOmitNullService _nullAttributeValues; + private readonly IDefaultsService _defaultsService; + private readonly INullsService _nullsService; - public ResourceObjectBuilderSettingsProvider(IOmitDefaultService defaultAttributeValues, - IOmitNullService nullAttributeValues) + public ResourceObjectBuilderSettingsProvider(IDefaultsService defaultsService, + INullsService nullsService) { - _defaultAttributeValues = defaultAttributeValues; - _nullAttributeValues = nullAttributeValues; + _defaultsService = defaultsService; + _nullsService = nullsService; } /// public ResourceObjectBuilderSettings Get() { - return new ResourceObjectBuilderSettings(_nullAttributeValues.OmitAttributeIfValueIsNull, _defaultAttributeValues.OmitAttributeIfValueIsDefault); + return new ResourceObjectBuilderSettings(_nullsService.OmitAttributeIfValueIsNull, _defaultsService.OmitAttributeIfValueIsDefault); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs index 9e22588693..c448808ae9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs @@ -8,6 +8,8 @@ using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; using Xunit; @@ -16,24 +18,21 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility [Collection("WebHostCollection")] public sealed class OmitAttributeIfValueIsNullTests : IAsyncLifetime { - private readonly TestFixture _fixture; private readonly AppDbContext _dbContext; private readonly TodoItem _todoItem; public OmitAttributeIfValueIsNullTests(TestFixture fixture) { - _fixture = fixture; _dbContext = fixture.GetService(); - var person = new Person { FirstName = "Bob", LastName = null }; - _todoItem = new TodoItem + var todoItem = new TodoItem { Description = null, Ordinal = 1, CreatedDate = DateTime.Now, AchievedDate = DateTime.Now.AddDays(2), - Owner = person + Owner = new Person { FirstName = "Bob", LastName = null } }; - _todoItem = _dbContext.TodoItems.Add(_todoItem).Entity; + _todoItem = _dbContext.TodoItems.Add(todoItem).Entity; } public async Task InitializeAsync() @@ -49,14 +48,14 @@ public Task DisposeAsync() [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(false, true, "false", true)] + [InlineData(false, false, "false", false)] + [InlineData(true, true, "true", false)] + [InlineData(true, false, "true", true)] [InlineData(null, false, "true", false)] - [InlineData(null, true, "true", true)] - [InlineData(null, true, "false", false)] + [InlineData(null, false, "false", false)] + [InlineData(null, true, "false", true)] + [InlineData(null, true, "true", false)] [InlineData(null, true, "this-is-not-a-boolean-value", false)] [InlineData(null, false, "this-is-not-a-boolean-value", false)] [InlineData(true, true, "this-is-not-a-boolean-value", true)] @@ -66,27 +65,32 @@ public Task DisposeAsync() public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, bool? allowQueryStringOverride, string queryStringOverride, bool expectNullsMissing) { - var jsonApiOptions = _fixture.GetService(); + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var services = server.Host.Services; + var client = server.CreateClient(); + + var options = (IJsonApiOptions)services.GetService(typeof(IJsonApiOptions)); + if (omitAttributeIfValueIsNull != null) { - jsonApiOptions.SerializerSettings.NullValueHandling = omitAttributeIfValueIsNull.Value + options.SerializerSettings.NullValueHandling = omitAttributeIfValueIsNull.Value ? NullValueHandling.Ignore : NullValueHandling.Include; } if (allowQueryStringOverride != null) { - jsonApiOptions.AllowOmitNullQueryStringOverride = allowQueryStringOverride.Value; + options.AllowQueryStringOverrideForSerializerNullValueHandling = allowQueryStringOverride.Value; } - var httpMethod = new HttpMethod("GET"); var queryString = allowQueryStringOverride != null - ? $"&omitNull={queryStringOverride}" + ? $"&nulls={queryStringOverride}" : ""; var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}"; - var request = new HttpRequestMessage(httpMethod, route); + var request = new HttpRequestMessage(HttpMethod.Get, route); // Act - var response = await _fixture.Client.SendAsync(request); + var response = await client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); var isQueryStringMissing = queryString.Length > 0 && queryStringOverride == null; @@ -101,8 +105,8 @@ public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); Assert.Equal("Usage of one or more query string parameters is not allowed at the requested endpoint.", errorDocument.Errors[0].Title); - Assert.Equal("The parameter 'omitNull' cannot be used at this endpoint.", errorDocument.Errors[0].Detail); - Assert.Equal("omitNull", errorDocument.Errors[0].Source.Parameter); + Assert.Equal("The parameter 'nulls' cannot be used at this endpoint.", errorDocument.Errors[0].Detail); + Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); } else if (isQueryStringMissing) { @@ -112,8 +116,8 @@ public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); Assert.Equal("Missing query string parameter value.", errorDocument.Errors[0].Title); - Assert.Equal("Missing value for 'omitNull' query string parameter.", errorDocument.Errors[0].Detail); - Assert.Equal("omitNull", errorDocument.Errors[0].Source.Parameter); + Assert.Equal("Missing value for 'nulls' query string parameter.", errorDocument.Errors[0].Detail); + Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); } else if (isQueryStringInvalid) { @@ -123,8 +127,8 @@ public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, Assert.Single(errorDocument.Errors); Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); Assert.Equal("The specified query string value must be 'true' or 'false'.", errorDocument.Errors[0].Title); - Assert.Equal("The value 'this-is-not-a-boolean-value' for parameter 'omitNull' is not a valid boolean value.", errorDocument.Errors[0].Detail); - Assert.Equal("omitNull", errorDocument.Errors[0].Source.Parameter); + Assert.Equal("The value 'this-is-not-a-boolean-value' for parameter 'nulls' is not a valid boolean value.", errorDocument.Errors[0].Detail); + Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); } else { diff --git a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs b/test/UnitTests/QueryParameters/DefaultsServiceTests.cs similarity index 64% rename from test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs rename to test/UnitTests/QueryParameters/DefaultsServiceTests.cs index c1d29c7896..e9c686c2b2 100644 --- a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs +++ b/test/UnitTests/QueryParameters/DefaultsServiceTests.cs @@ -10,58 +10,58 @@ namespace UnitTests.QueryParameters { - public sealed class OmitDefaultServiceTests : QueryParametersUnitTestCollection + public sealed class DefaultsServiceTests : QueryParametersUnitTestCollection { - public OmitDefaultService GetService(bool @default, bool @override) + public DefaultsService GetService(bool defaultValue, bool allowOverride) { var options = new JsonApiOptions { SerializerSettings = { - DefaultValueHandling = @default ? DefaultValueHandling.Ignore : DefaultValueHandling.Include + DefaultValueHandling = defaultValue ? DefaultValueHandling.Ignore : DefaultValueHandling.Include }, - AllowOmitDefaultQueryStringOverride = @override + AllowQueryStringOverrideForSerializerDefaultValueHandling = allowOverride }; - return new OmitDefaultService(options); + return new DefaultsService(options); } [Fact] - public void CanParse_OmitDefaultService_SucceedOnMatch() + public void CanParse_DefaultsService_SucceedOnMatch() { // Arrange var service = GetService(true, true); // Act - bool result = service.CanParse("omitDefault"); + bool result = service.CanParse("defaults"); // Assert Assert.True(result); } [Fact] - public void CanParse_OmitDefaultService_FailOnMismatch() + public void CanParse_DefaultsService_FailOnMismatch() { // Arrange var service = GetService(true, true); // Act - bool result = service.CanParse("omit-default"); + bool result = service.CanParse("defaultsettings"); // Assert Assert.False(result); } [Theory] - [InlineData("false", true, true, false)] - [InlineData("false", true, false, true)] - [InlineData("true", false, true, true)] - [InlineData("true", false, false, false)] - public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool @default, bool @override, bool expected) + [InlineData("true", true, true, false)] + [InlineData("true", true, false, true)] + [InlineData("false", false, true, true)] + [InlineData("false", false, false, false)] + public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool defaultValue, bool allowOverride, bool expected) { // Arrange - var query = new KeyValuePair("omitDefault", queryValue); - var service = GetService(@default, @override); + var query = new KeyValuePair("defaults", queryValue); + var service = GetService(defaultValue, allowOverride); // Act if (service.CanParse(query.Key) && service.IsEnabled(DisableQueryAttribute.Empty)) @@ -74,10 +74,10 @@ public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool @d } [Fact] - public void Parse_OmitDefaultService_FailOnNonBooleanValue() + public void Parse_DefaultsService_FailOnNonBooleanValue() { // Arrange - const string parameterName = "omit-default"; + const string parameterName = "defaults"; var service = GetService(true, true); // Act, assert diff --git a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs b/test/UnitTests/QueryParameters/NullsServiceTests.cs similarity index 65% rename from test/UnitTests/QueryParameters/OmitNullServiceTests.cs rename to test/UnitTests/QueryParameters/NullsServiceTests.cs index 740404c90c..cf86dc41a0 100644 --- a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs +++ b/test/UnitTests/QueryParameters/NullsServiceTests.cs @@ -10,58 +10,58 @@ namespace UnitTests.QueryParameters { - public sealed class OmitNullServiceTests : QueryParametersUnitTestCollection + public sealed class NullsServiceTests : QueryParametersUnitTestCollection { - public OmitNullService GetService(bool @default, bool @override) + public NullsService GetService(bool defaultValue, bool allowOverride) { var options = new JsonApiOptions { SerializerSettings = { - NullValueHandling = @default ? NullValueHandling.Ignore : NullValueHandling.Include + NullValueHandling = defaultValue ? NullValueHandling.Ignore : NullValueHandling.Include }, - AllowOmitNullQueryStringOverride = @override + AllowQueryStringOverrideForSerializerNullValueHandling = allowOverride }; - return new OmitNullService(options); + return new NullsService(options); } [Fact] - public void CanParse_OmitNullService_SucceedOnMatch() + public void CanParse_NullsService_SucceedOnMatch() { // Arrange var service = GetService(true, true); // Act - bool result = service.CanParse("omitNull"); + bool result = service.CanParse("nulls"); // Assert Assert.True(result); } [Fact] - public void CanParse_OmitNullService_FailOnMismatch() + public void CanParse_NullsService_FailOnMismatch() { // Arrange var service = GetService(true, true); // Act - bool result = service.CanParse("omit-null"); + bool result = service.CanParse("nullsettings"); // Assert Assert.False(result); } [Theory] - [InlineData("false", true, true, false)] - [InlineData("false", true, false, true)] - [InlineData("true", false, true, true)] - [InlineData("true", false, false, false)] - public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool @default, bool @override, bool expected) + [InlineData("true", true, true, false)] + [InlineData("true", true, false, true)] + [InlineData("false", false, true, true)] + [InlineData("false", false, false, false)] + public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool defaultValue, bool allowOverride, bool expected) { // Arrange - var query = new KeyValuePair("omitNull", queryValue); - var service = GetService(@default, @override); + var query = new KeyValuePair("nulls", queryValue); + var service = GetService(defaultValue, allowOverride); // Act if (service.CanParse(query.Key) && service.IsEnabled(DisableQueryAttribute.Empty)) @@ -74,10 +74,10 @@ public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool @d } [Fact] - public void Parse_OmitNullService_FailOnNonBooleanValue() + public void Parse_NullsService_FailOnNonBooleanValue() { // Arrange - const string parameterName = "omit-null"; + const string parameterName = "nulls"; var service = GetService(true, true); // Act, assert From 2393bc68dc03b7dbfc96cf92e9bb464864f043f0 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 10 Apr 2020 15:18:13 +0200 Subject: [PATCH 07/11] Fixed: update deep property on shared settings that were only shallow-copied breaks with concurrent requests --- .../Extensions/JsonSerializerExtensions.cs | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs index 0aba5e5df5..eba2b8f65f 100644 --- a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs @@ -9,9 +9,36 @@ public static void ApplyErrorSettings(this JsonSerializer jsonSerializer) { jsonSerializer.NullValueHandling = NullValueHandling.Ignore; - var contractResolver = (DefaultContractResolver)jsonSerializer.ContractResolver; - contractResolver.NamingStrategy.ProcessDictionaryKeys = true; - contractResolver.NamingStrategy.ProcessExtensionDataNames = true; + // JsonSerializer.Create() only performs a shallow copy of the shared settings, so we cannot change properties on its ContractResolver. + // But to serialize ErrorMeta.Data correctly, we need to ensure that JsonSerializer.ContractResolver.NamingStrategy.ProcessExtensionDataNames + // is set to 'true' while serializing errors. + var sharedContractResolver = (DefaultContractResolver)jsonSerializer.ContractResolver; + + jsonSerializer.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new AlwaysProcessExtensionDataNamingStrategyWrapper(sharedContractResolver.NamingStrategy) + }; + } + + private sealed class AlwaysProcessExtensionDataNamingStrategyWrapper : NamingStrategy + { + private readonly NamingStrategy _namingStrategy; + + public AlwaysProcessExtensionDataNamingStrategyWrapper(NamingStrategy namingStrategy) + { + _namingStrategy = namingStrategy ?? new DefaultNamingStrategy(); + } + + public override string GetExtensionDataName(string name) + { + // Ignore the value of ProcessExtensionDataNames property on the wrapped strategy (short-circuit). + return ResolvePropertyName(name); + } + + protected override string ResolvePropertyName(string name) + { + return _namingStrategy.GetPropertyName(name, false); + } } } } From 4a0edd967cb6158bd652b2a5427aa11dec22dea2 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 10 Apr 2020 17:37:02 +0200 Subject: [PATCH 08/11] Replaced internal booleans with enums to improve readability. Updated tests to cover all cases. Bugfix where default setting was never used, because the expression default(object) equals null, instead of the default value of the runtime type. --- src/JsonApiDotNetCore/Internal/TypeHelper.cs | 16 +- .../Contracts/IDefaultsService.cs | 4 +- .../Contracts/INullsService.cs | 4 +- .../QueryParameterServices/DefaultsService.cs | 7 +- .../QueryParameterServices/NullsService.cs | 6 +- .../Common/ResourceObjectBuilder.cs | 18 +- .../Common/ResourceObjectBuilderSettings.cs | 41 +---- .../ResourceObjectBuilderSettingsProvider.cs | 5 +- .../Server/ResponseSerializer.cs | 4 +- .../Extensibility/IgnoreDefaultValuesTests.cs | 171 +++++++++++++++++ .../Extensibility/IgnoreNullValuesTests.cs | 174 ++++++++++++++++++ .../OmitAttributeIfValueIsNullTests.cs | 144 --------------- .../QueryParameters/DefaultsServiceTests.cs | 37 ++-- .../QueryParameters/NullsServiceTests.cs | 37 ++-- 14 files changed, 426 insertions(+), 242 deletions(-) create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs delete mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs diff --git a/src/JsonApiDotNetCore/Internal/TypeHelper.cs b/src/JsonApiDotNetCore/Internal/TypeHelper.cs index d68bcde72e..22f62e47b0 100644 --- a/src/JsonApiDotNetCore/Internal/TypeHelper.cs +++ b/src/JsonApiDotNetCore/Internal/TypeHelper.cs @@ -15,6 +15,7 @@ private static bool IsNullable(Type type) { return (!type.IsValueType || Nullable.GetUnderlyingType(type) != null); } + public static object ConvertType(object value, Type type) { if (value == null && !IsNullable(type)) @@ -35,7 +36,7 @@ public static object ConvertType(object value, Type type) var stringValue = value.ToString(); if (string.IsNullOrEmpty(stringValue)) - return GetDefaultType(type); + return GetDefaultValue(type); if (type == typeof(Guid)) return Guid.Parse(stringValue); @@ -58,18 +59,9 @@ public static object ConvertType(object value, Type type) } } - private static object GetDefaultType(Type type) - { - if (type.IsValueType) - { - return type.New(); - } - return null; - } - - public static T ConvertType(object value) + internal static object GetDefaultValue(this Type type) { - return (T)ConvertType(value, typeof(T)); + return type.IsValueType ? type.New() : null; } public static Type GetTypeOfList(Type type) diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs index 842f245db4..0d69326296 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; + namespace JsonApiDotNetCore.Query { /// @@ -8,6 +10,6 @@ public interface IDefaultsService : IQueryParameterService /// /// Contains the effective value of default configuration and query string override, after parsing has occured. /// - bool OmitAttributeIfValueIsDefault { get; } + DefaultValueHandling SerializerDefaultValueHandling { get; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs index 1a24070b02..038eaa7153 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs @@ -1,3 +1,5 @@ +using Newtonsoft.Json; + namespace JsonApiDotNetCore.Query { /// @@ -8,6 +10,6 @@ public interface INullsService : IQueryParameterService /// /// Contains the effective value of default configuration and query string override, after parsing has occured. /// - bool OmitAttributeIfValueIsNull { get; } + NullValueHandling SerializerNullValueHandling { get; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs index 0062360713..aa8c597884 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs @@ -13,13 +13,14 @@ public class DefaultsService : QueryParameterService, IDefaultsService public DefaultsService(IJsonApiOptions options) { - OmitAttributeIfValueIsDefault = options.SerializerSettings.DefaultValueHandling == DefaultValueHandling.Ignore; + SerializerDefaultValueHandling = options.SerializerSettings.DefaultValueHandling; _options = options; } /// - public bool OmitAttributeIfValueIsDefault { get; private set; } + public DefaultValueHandling SerializerDefaultValueHandling { get; private set; } + /// public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) { return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && @@ -42,7 +43,7 @@ public virtual void Parse(string parameterName, StringValues parameterValue) $"The value '{parameterValue}' for parameter '{parameterName}' is not a valid boolean value."); } - OmitAttributeIfValueIsDefault = !result; + SerializerDefaultValueHandling = result ? DefaultValueHandling.Include : DefaultValueHandling.Ignore; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs index 6e71b0bdb1..df3f06a14e 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs @@ -13,12 +13,12 @@ public class NullsService : QueryParameterService, INullsService public NullsService(IJsonApiOptions options) { - OmitAttributeIfValueIsNull = options.SerializerSettings.NullValueHandling == NullValueHandling.Ignore; + SerializerNullValueHandling = options.SerializerSettings.NullValueHandling; _options = options; } /// - public bool OmitAttributeIfValueIsNull { get; private set; } + public NullValueHandling SerializerNullValueHandling { get; private set; } /// public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) @@ -43,7 +43,7 @@ public virtual void Parse(string parameterName, StringValues parameterValue) $"The value '{parameterValue}' for parameter '{parameterName}' is not a valid boolean value."); } - OmitAttributeIfValueIsNull = !result; + SerializerNullValueHandling = result ? NullValueHandling.Include : NullValueHandling.Ignore; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs index d4942d5c59..bf45416449 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs @@ -2,8 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -138,9 +140,19 @@ private void ProcessAttributes(IIdentifiable entity, IEnumerable ro.Attributes = new Dictionary(); foreach (var attr in attributes) { - var value = attr.GetValue(entity); - if (!(value == default && _settings.OmitAttributeIfValueIsDefault) && !(value == null && _settings.OmitAttributeIfValueIsNull)) - ro.Attributes.Add(attr.PublicAttributeName, value); + object value = attr.GetValue(entity); + + if (_settings.SerializerNullValueHandling == NullValueHandling.Ignore && value == null) + { + return; + } + + if (_settings.SerializerDefaultValueHandling == DefaultValueHandling.Ignore && value == attr.PropertyInfo.PropertyType.GetDefaultValue()) + { + return; + } + + ro.Attributes.Add(attr.PublicAttributeName, value); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs index 2be257467e..2eceb3809d 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs @@ -1,5 +1,5 @@ -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -7,38 +7,17 @@ namespace JsonApiDotNetCore.Serialization /// Options used to configure how fields of a model get serialized into /// a json:api . /// - public sealed class ResourceObjectBuilderSettings + public sealed class ResourceObjectBuilderSettings { - /// Omit null values from attributes - /// Omit default values from attributes - public ResourceObjectBuilderSettings(bool omitAttributeIfValueIsNull = false, bool omitAttributeIfValueIsDefault = false) + public NullValueHandling SerializerNullValueHandling { get; } + public DefaultValueHandling SerializerDefaultValueHandling { get; } + + public ResourceObjectBuilderSettings( + NullValueHandling serializerNullValueHandling = NullValueHandling.Include, + DefaultValueHandling serializerDefaultValueHandling = DefaultValueHandling.Include) { - OmitAttributeIfValueIsNull = omitAttributeIfValueIsNull; - OmitAttributeIfValueIsDefault = omitAttributeIfValueIsDefault; + SerializerNullValueHandling = serializerNullValueHandling; + SerializerDefaultValueHandling = serializerDefaultValueHandling; } - - /// - /// Prevent attributes with null values from being included in the response. - /// This property is internal and if you want to enable this behavior, you - /// should do so on the . - /// - /// - /// - /// options.NullAttributeResponseBehavior = new NullAttributeResponseBehavior(true); - /// - /// - public bool OmitAttributeIfValueIsNull { get; } - - /// - /// Prevent attributes with default values from being included in the response. - /// This property is internal and if you want to enable this behavior, you - /// should do so on the . - /// - /// - /// - /// options.DefaultAttributeResponseBehavior = new DefaultAttributeResponseBehavior(true); - /// - /// - public bool OmitAttributeIfValueIsDefault { get; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs index 0acece0d30..60f88112d3 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs @@ -12,8 +12,7 @@ public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuild private readonly IDefaultsService _defaultsService; private readonly INullsService _nullsService; - public ResourceObjectBuilderSettingsProvider(IDefaultsService defaultsService, - INullsService nullsService) + public ResourceObjectBuilderSettingsProvider(IDefaultsService defaultsService, INullsService nullsService) { _defaultsService = defaultsService; _nullsService = nullsService; @@ -22,7 +21,7 @@ public ResourceObjectBuilderSettingsProvider(IDefaultsService defaultsService, /// public ResourceObjectBuilderSettings Get() { - return new ResourceObjectBuilderSettings(_nullsService.OmitAttributeIfValueIsNull, _defaultsService.OmitAttributeIfValueIsDefault); + return new ResourceObjectBuilderSettings(_nullsService.SerializerNullValueHandling, _defaultsService.SerializerDefaultValueHandling); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs index 66d4f77111..720f782f49 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Graph; using JsonApiDotNetCore.Models; using Newtonsoft.Json; using JsonApiDotNetCore.Managers.Contracts; @@ -12,7 +11,6 @@ namespace JsonApiDotNetCore.Serialization.Server { - /// /// Server serializer implementation of /// @@ -124,7 +122,7 @@ internal string SerializeMany(IEnumerable entities) /// /// Gets the list of attributes to serialize for the given . - /// Note that the choice omitting null-values is not handled here, + /// Note that the choice of omitting null/default-values is not handled here, /// but in . /// /// Type of entity to be serialized diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs new file mode 100644 index 0000000000..e1c7063789 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreDefaultValuesTests.cs @@ -0,0 +1,171 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + [Collection("WebHostCollection")] + public sealed class IgnoreDefaultValuesTests : IAsyncLifetime + { + private readonly AppDbContext _dbContext; + private readonly TodoItem _todoItem; + + public IgnoreDefaultValuesTests(TestFixture fixture) + { + _dbContext = fixture.GetService(); + var todoItem = new TodoItem + { + CreatedDate = default, + Owner = new Person { Age = default } + }; + _todoItem = _dbContext.TodoItems.Add(todoItem).Entity; + } + + public async Task InitializeAsync() + { + await _dbContext.SaveChangesAsync(); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + + [Theory] + [InlineData(null, null, null, DefaultValueHandling.Include)] + [InlineData(null, null, "false", DefaultValueHandling.Include)] + [InlineData(null, null, "true", DefaultValueHandling.Include)] + [InlineData(null, null, "unknown", null)] + [InlineData(null, null, "", null)] + [InlineData(null, false, null, DefaultValueHandling.Include)] + [InlineData(null, false, "false", DefaultValueHandling.Include)] + [InlineData(null, false, "true", DefaultValueHandling.Include)] + [InlineData(null, false, "unknown", null)] + [InlineData(null, false, "", null)] + [InlineData(null, true, null, DefaultValueHandling.Include)] + [InlineData(null, true, "false", DefaultValueHandling.Ignore)] + [InlineData(null, true, "true", DefaultValueHandling.Include)] + [InlineData(null, true, "unknown", null)] + [InlineData(null, true, "", null)] + [InlineData(DefaultValueHandling.Ignore, null, null, DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, null, "false", DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, null, "true", DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, null, "unknown", null)] + [InlineData(DefaultValueHandling.Ignore, null, "", null)] + [InlineData(DefaultValueHandling.Ignore, false, null, DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, false, "false", DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, false, "true", DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, false, "unknown", null)] + [InlineData(DefaultValueHandling.Ignore, false, "", null)] + [InlineData(DefaultValueHandling.Ignore, true, null, DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, true, "false", DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Ignore, true, "true", DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Ignore, true, "unknown", null)] + [InlineData(DefaultValueHandling.Ignore, true, "", null)] + [InlineData(DefaultValueHandling.Include, null, null, DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, null, "false", DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, null, "true", DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, null, "unknown", null)] + [InlineData(DefaultValueHandling.Include, null, "", null)] + [InlineData(DefaultValueHandling.Include, false, null, DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, false, "false", DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, false, "true", DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, false, "unknown", null)] + [InlineData(DefaultValueHandling.Include, false, "", null)] + [InlineData(DefaultValueHandling.Include, true, null, DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, true, "false", DefaultValueHandling.Ignore)] + [InlineData(DefaultValueHandling.Include, true, "true", DefaultValueHandling.Include)] + [InlineData(DefaultValueHandling.Include, true, "unknown", null)] + [InlineData(DefaultValueHandling.Include, true, "", null)] + public async Task CheckBehaviorCombination(DefaultValueHandling? defaultValue, bool? allowQueryStringOverride, string queryStringValue, DefaultValueHandling? expected) + { + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var services = server.Host.Services; + var client = server.CreateClient(); + + var options = (IJsonApiOptions)services.GetService(typeof(IJsonApiOptions)); + + if (defaultValue != null) + { + options.SerializerSettings.DefaultValueHandling = defaultValue.Value; + } + if (allowQueryStringOverride != null) + { + options.AllowQueryStringOverrideForSerializerDefaultValueHandling = allowQueryStringOverride.Value; + } + + var queryString = queryStringValue != null + ? $"&defaults={queryStringValue}" + : ""; + var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}"; + var request = new HttpRequestMessage(HttpMethod.Get, route); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var isQueryStringValueEmpty = queryStringValue == string.Empty; + var isDisallowedOverride = options.AllowQueryStringOverrideForSerializerDefaultValueHandling == false && queryStringValue != null; + var isQueryStringInvalid = queryStringValue != null && !bool.TryParse(queryStringValue, out _); + + if (isQueryStringValueEmpty) + { + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); + Assert.Equal("Missing query string parameter value.", errorDocument.Errors[0].Title); + Assert.Equal("Missing value for 'defaults' query string parameter.", errorDocument.Errors[0].Detail); + Assert.Equal("defaults", errorDocument.Errors[0].Source.Parameter); + } + else if (isDisallowedOverride) + { + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); + Assert.Equal("Usage of one or more query string parameters is not allowed at the requested endpoint.", errorDocument.Errors[0].Title); + Assert.Equal("The parameter 'defaults' cannot be used at this endpoint.", errorDocument.Errors[0].Detail); + Assert.Equal("defaults", errorDocument.Errors[0].Source.Parameter); + } + else if (isQueryStringInvalid) + { + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); + Assert.Equal("The specified query string value must be 'true' or 'false'.", errorDocument.Errors[0].Title); + Assert.Equal("The value 'unknown' for parameter 'defaults' is not a valid boolean value.", errorDocument.Errors[0].Detail); + Assert.Equal("defaults", errorDocument.Errors[0].Source.Parameter); + } + else + { + if (expected == null) + { + throw new Exception("Invalid test combination. Should never get here."); + } + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var deserializeBody = JsonConvert.DeserializeObject(body); + Assert.Equal(expected == DefaultValueHandling.Include, deserializeBody.SingleData.Attributes.ContainsKey("createdDate")); + Assert.Equal(expected == DefaultValueHandling.Include, deserializeBody.Included[0].Attributes.ContainsKey("the-Age")); + } + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs new file mode 100644 index 0000000000..83f4bc9d5f --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/IgnoreNullValuesTests.cs @@ -0,0 +1,174 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCoreExample; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility +{ + [Collection("WebHostCollection")] + public sealed class IgnoreNullValuesTests : IAsyncLifetime + { + private readonly AppDbContext _dbContext; + private readonly TodoItem _todoItem; + + public IgnoreNullValuesTests(TestFixture fixture) + { + _dbContext = fixture.GetService(); + var todoItem = new TodoItem + { + Description = null, + Ordinal = 1, + CreatedDate = DateTime.Now, + AchievedDate = DateTime.Now.AddDays(2), + Owner = new Person { FirstName = "Bob", LastName = null } + }; + _todoItem = _dbContext.TodoItems.Add(todoItem).Entity; + } + + public async Task InitializeAsync() + { + await _dbContext.SaveChangesAsync(); + } + + public Task DisposeAsync() + { + return Task.CompletedTask; + } + + [Theory] + [InlineData(null, null, null, NullValueHandling.Include)] + [InlineData(null, null, "false", NullValueHandling.Include)] + [InlineData(null, null, "true", NullValueHandling.Include)] + [InlineData(null, null, "unknown", null)] + [InlineData(null, null, "", null)] + [InlineData(null, false, null, NullValueHandling.Include)] + [InlineData(null, false, "false", NullValueHandling.Include)] + [InlineData(null, false, "true", NullValueHandling.Include)] + [InlineData(null, false, "unknown", null)] + [InlineData(null, false, "", null)] + [InlineData(null, true, null, NullValueHandling.Include)] + [InlineData(null, true, "false", NullValueHandling.Ignore)] + [InlineData(null, true, "true", NullValueHandling.Include)] + [InlineData(null, true, "unknown", null)] + [InlineData(null, true, "", null)] + [InlineData(NullValueHandling.Ignore, null, null, NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, null, "false", NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, null, "true", NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, null, "unknown", null)] + [InlineData(NullValueHandling.Ignore, null, "", null)] + [InlineData(NullValueHandling.Ignore, false, null, NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, false, "false", NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, false, "true", NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, false, "unknown", null)] + [InlineData(NullValueHandling.Ignore, false, "", null)] + [InlineData(NullValueHandling.Ignore, true, null, NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, true, "false", NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Ignore, true, "true", NullValueHandling.Include)] + [InlineData(NullValueHandling.Ignore, true, "unknown", null)] + [InlineData(NullValueHandling.Ignore, true, "", null)] + [InlineData(NullValueHandling.Include, null, null, NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, null, "false", NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, null, "true", NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, null, "unknown", null)] + [InlineData(NullValueHandling.Include, null, "", null)] + [InlineData(NullValueHandling.Include, false, null, NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, false, "false", NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, false, "true", NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, false, "unknown", null)] + [InlineData(NullValueHandling.Include, false, "", null)] + [InlineData(NullValueHandling.Include, true, null, NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, true, "false", NullValueHandling.Ignore)] + [InlineData(NullValueHandling.Include, true, "true", NullValueHandling.Include)] + [InlineData(NullValueHandling.Include, true, "unknown", null)] + [InlineData(NullValueHandling.Include, true, "", null)] + public async Task CheckBehaviorCombination(NullValueHandling? defaultValue, bool? allowQueryStringOverride, string queryStringValue, NullValueHandling? expected) + { + var builder = new WebHostBuilder().UseStartup(); + var server = new TestServer(builder); + var services = server.Host.Services; + var client = server.CreateClient(); + + var options = (IJsonApiOptions)services.GetService(typeof(IJsonApiOptions)); + + if (defaultValue != null) + { + options.SerializerSettings.NullValueHandling = defaultValue.Value; + } + if (allowQueryStringOverride != null) + { + options.AllowQueryStringOverrideForSerializerNullValueHandling = allowQueryStringOverride.Value; + } + + var queryString = queryStringValue != null + ? $"&nulls={queryStringValue}" + : ""; + var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}"; + var request = new HttpRequestMessage(HttpMethod.Get, route); + + // Act + var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + var isQueryStringValueEmpty = queryStringValue == string.Empty; + var isDisallowedOverride = options.AllowQueryStringOverrideForSerializerNullValueHandling == false && queryStringValue != null; + var isQueryStringInvalid = queryStringValue != null && !bool.TryParse(queryStringValue, out _); + + if (isQueryStringValueEmpty) + { + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); + Assert.Equal("Missing query string parameter value.", errorDocument.Errors[0].Title); + Assert.Equal("Missing value for 'nulls' query string parameter.", errorDocument.Errors[0].Detail); + Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); + } + else if (isDisallowedOverride) + { + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); + Assert.Equal("Usage of one or more query string parameters is not allowed at the requested endpoint.", errorDocument.Errors[0].Title); + Assert.Equal("The parameter 'nulls' cannot be used at this endpoint.", errorDocument.Errors[0].Detail); + Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); + } + else if (isQueryStringInvalid) + { + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); + Assert.Equal("The specified query string value must be 'true' or 'false'.", errorDocument.Errors[0].Title); + Assert.Equal("The value 'unknown' for parameter 'nulls' is not a valid boolean value.", errorDocument.Errors[0].Detail); + Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); + } + else + { + if (expected == null) + { + throw new Exception("Invalid test combination. Should never get here."); + } + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var deserializeBody = JsonConvert.DeserializeObject(body); + Assert.Equal(expected == NullValueHandling.Include, deserializeBody.SingleData.Attributes.ContainsKey("description")); + Assert.Equal(expected == NullValueHandling.Include, deserializeBody.Included[0].Attributes.ContainsKey("lastName")); + } + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs deleted file mode 100644 index c448808ae9..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.JsonApiDocuments; -using JsonApiDotNetCoreExample; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Extensibility -{ - [Collection("WebHostCollection")] - public sealed class OmitAttributeIfValueIsNullTests : IAsyncLifetime - { - private readonly AppDbContext _dbContext; - private readonly TodoItem _todoItem; - - public OmitAttributeIfValueIsNullTests(TestFixture fixture) - { - _dbContext = fixture.GetService(); - var todoItem = new TodoItem - { - Description = null, - Ordinal = 1, - CreatedDate = DateTime.Now, - AchievedDate = DateTime.Now.AddDays(2), - Owner = new Person { FirstName = "Bob", LastName = null } - }; - _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, "false", true)] - [InlineData(false, false, "false", false)] - [InlineData(true, true, "true", false)] - [InlineData(true, false, "true", true)] - [InlineData(null, false, "true", false)] - [InlineData(null, false, "false", false)] - [InlineData(null, true, "false", true)] - [InlineData(null, true, "true", false)] - [InlineData(null, true, "this-is-not-a-boolean-value", false)] - [InlineData(null, false, "this-is-not-a-boolean-value", false)] - [InlineData(true, true, "this-is-not-a-boolean-value", true)] - [InlineData(true, false, "this-is-not-a-boolean-value", true)] - [InlineData(null, true, null, false)] - [InlineData(null, false, null, false)] - public async Task CheckNullBehaviorCombination(bool? omitAttributeIfValueIsNull, bool? allowQueryStringOverride, - string queryStringOverride, bool expectNullsMissing) - { - var builder = new WebHostBuilder().UseStartup(); - var server = new TestServer(builder); - var services = server.Host.Services; - var client = server.CreateClient(); - - var options = (IJsonApiOptions)services.GetService(typeof(IJsonApiOptions)); - - if (omitAttributeIfValueIsNull != null) - { - options.SerializerSettings.NullValueHandling = omitAttributeIfValueIsNull.Value - ? NullValueHandling.Ignore - : NullValueHandling.Include; - } - if (allowQueryStringOverride != null) - { - options.AllowQueryStringOverrideForSerializerNullValueHandling = allowQueryStringOverride.Value; - } - - var queryString = allowQueryStringOverride != null - ? $"&nulls={queryStringOverride}" - : ""; - var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}"; - var request = new HttpRequestMessage(HttpMethod.Get, route); - - // Act - var response = await client.SendAsync(request); - var body = await response.Content.ReadAsStringAsync(); - - var isQueryStringMissing = queryString.Length > 0 && queryStringOverride == null; - var isQueryStringInvalid = queryString.Length > 0 && queryStringOverride != null && !bool.TryParse(queryStringOverride, out _); - var isDisallowedOverride = allowQueryStringOverride == false && queryStringOverride != null; - - if (isDisallowedOverride) - { - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); - Assert.Equal("Usage of one or more query string parameters is not allowed at the requested endpoint.", errorDocument.Errors[0].Title); - Assert.Equal("The parameter 'nulls' cannot be used at this endpoint.", errorDocument.Errors[0].Detail); - Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); - } - else if (isQueryStringMissing) - { - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); - Assert.Equal("Missing query string parameter value.", errorDocument.Errors[0].Title); - Assert.Equal("Missing value for 'nulls' query string parameter.", errorDocument.Errors[0].Detail); - Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); - } - else if (isQueryStringInvalid) - { - Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.BadRequest, errorDocument.Errors[0].StatusCode); - Assert.Equal("The specified query string value must be 'true' or 'false'.", errorDocument.Errors[0].Title); - Assert.Equal("The value 'this-is-not-a-boolean-value' for parameter 'nulls' is not a valid boolean value.", errorDocument.Errors[0].Detail); - Assert.Equal("nulls", errorDocument.Errors[0].Source.Parameter); - } - else - { - // Assert: does response contain a null valued attribute? - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var deserializeBody = JsonConvert.DeserializeObject(body); - Assert.Equal(expectNullsMissing, !deserializeBody.SingleData.Attributes.ContainsKey("description")); - Assert.Equal(expectNullsMissing, !deserializeBody.Included[0].Attributes.ContainsKey("lastName")); - } - } - } -} diff --git a/test/UnitTests/QueryParameters/DefaultsServiceTests.cs b/test/UnitTests/QueryParameters/DefaultsServiceTests.cs index e9c686c2b2..a324427fa9 100644 --- a/test/UnitTests/QueryParameters/DefaultsServiceTests.cs +++ b/test/UnitTests/QueryParameters/DefaultsServiceTests.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; using System.Net; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Query; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using Xunit; @@ -12,14 +10,11 @@ namespace UnitTests.QueryParameters { public sealed class DefaultsServiceTests : QueryParametersUnitTestCollection { - public DefaultsService GetService(bool defaultValue, bool allowOverride) + public DefaultsService GetService(DefaultValueHandling defaultValue, bool allowOverride) { var options = new JsonApiOptions { - SerializerSettings = - { - DefaultValueHandling = defaultValue ? DefaultValueHandling.Ignore : DefaultValueHandling.Include - }, + SerializerSettings = { DefaultValueHandling = defaultValue }, AllowQueryStringOverrideForSerializerDefaultValueHandling = allowOverride }; @@ -30,7 +25,7 @@ public DefaultsService GetService(bool defaultValue, bool allowOverride) public void CanParse_DefaultsService_SucceedOnMatch() { // Arrange - var service = GetService(true, true); + var service = GetService(DefaultValueHandling.Include, true); // Act bool result = service.CanParse("defaults"); @@ -43,7 +38,7 @@ public void CanParse_DefaultsService_SucceedOnMatch() public void CanParse_DefaultsService_FailOnMismatch() { // Arrange - var service = GetService(true, true); + var service = GetService(DefaultValueHandling.Include, true); // Act bool result = service.CanParse("defaultsettings"); @@ -53,24 +48,28 @@ public void CanParse_DefaultsService_FailOnMismatch() } [Theory] - [InlineData("true", true, true, false)] - [InlineData("true", true, false, true)] - [InlineData("false", false, true, true)] - [InlineData("false", false, false, false)] - public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool defaultValue, bool allowOverride, bool expected) + [InlineData("false", DefaultValueHandling.Ignore, false, DefaultValueHandling.Ignore)] + [InlineData("true", DefaultValueHandling.Ignore, false, DefaultValueHandling.Ignore)] + [InlineData("false", DefaultValueHandling.Include, false, DefaultValueHandling.Include)] + [InlineData("true", DefaultValueHandling.Include, false, DefaultValueHandling.Include)] + [InlineData("false", DefaultValueHandling.Ignore, true, DefaultValueHandling.Ignore)] + [InlineData("true", DefaultValueHandling.Ignore, true, DefaultValueHandling.Include)] + [InlineData("false", DefaultValueHandling.Include, true, DefaultValueHandling.Ignore)] + [InlineData("true", DefaultValueHandling.Include, true, DefaultValueHandling.Include)] + public void Parse_QueryConfigWithApiSettings_Succeeds(string queryValue, DefaultValueHandling defaultValue, bool allowOverride, DefaultValueHandling expected) { // Arrange - var query = new KeyValuePair("defaults", queryValue); + const string parameterName = "defaults"; var service = GetService(defaultValue, allowOverride); // Act - if (service.CanParse(query.Key) && service.IsEnabled(DisableQueryAttribute.Empty)) + if (service.CanParse(parameterName) && service.IsEnabled(DisableQueryAttribute.Empty)) { - service.Parse(query.Key, query.Value); + service.Parse(parameterName, queryValue); } // Assert - Assert.Equal(expected, service.OmitAttributeIfValueIsDefault); + Assert.Equal(expected, service.SerializerDefaultValueHandling); } [Fact] @@ -78,7 +77,7 @@ public void Parse_DefaultsService_FailOnNonBooleanValue() { // Arrange const string parameterName = "defaults"; - var service = GetService(true, true); + var service = GetService(DefaultValueHandling.Include, true); // Act, assert var exception = Assert.Throws(() => service.Parse(parameterName, "some")); diff --git a/test/UnitTests/QueryParameters/NullsServiceTests.cs b/test/UnitTests/QueryParameters/NullsServiceTests.cs index cf86dc41a0..8b65752918 100644 --- a/test/UnitTests/QueryParameters/NullsServiceTests.cs +++ b/test/UnitTests/QueryParameters/NullsServiceTests.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; using System.Net; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Query; -using Microsoft.Extensions.Primitives; using Newtonsoft.Json; using Xunit; @@ -12,14 +10,11 @@ namespace UnitTests.QueryParameters { public sealed class NullsServiceTests : QueryParametersUnitTestCollection { - public NullsService GetService(bool defaultValue, bool allowOverride) + public NullsService GetService(NullValueHandling defaultValue, bool allowOverride) { var options = new JsonApiOptions { - SerializerSettings = - { - NullValueHandling = defaultValue ? NullValueHandling.Ignore : NullValueHandling.Include - }, + SerializerSettings = { NullValueHandling = defaultValue }, AllowQueryStringOverrideForSerializerNullValueHandling = allowOverride }; @@ -30,7 +25,7 @@ public NullsService GetService(bool defaultValue, bool allowOverride) public void CanParse_NullsService_SucceedOnMatch() { // Arrange - var service = GetService(true, true); + var service = GetService(NullValueHandling.Include, true); // Act bool result = service.CanParse("nulls"); @@ -43,7 +38,7 @@ public void CanParse_NullsService_SucceedOnMatch() public void CanParse_NullsService_FailOnMismatch() { // Arrange - var service = GetService(true, true); + var service = GetService(NullValueHandling.Include, true); // Act bool result = service.CanParse("nullsettings"); @@ -53,24 +48,28 @@ public void CanParse_NullsService_FailOnMismatch() } [Theory] - [InlineData("true", true, true, false)] - [InlineData("true", true, false, true)] - [InlineData("false", false, true, true)] - [InlineData("false", false, false, false)] - public void Parse_QueryConfigWithApiSettings_CanParse(string queryValue, bool defaultValue, bool allowOverride, bool expected) + [InlineData("false", NullValueHandling.Ignore, false, NullValueHandling.Ignore)] + [InlineData("true", NullValueHandling.Ignore, false, NullValueHandling.Ignore)] + [InlineData("false", NullValueHandling.Include, false, NullValueHandling.Include)] + [InlineData("true", NullValueHandling.Include, false, NullValueHandling.Include)] + [InlineData("false", NullValueHandling.Ignore, true, NullValueHandling.Ignore)] + [InlineData("true", NullValueHandling.Ignore, true, NullValueHandling.Include)] + [InlineData("false", NullValueHandling.Include, true, NullValueHandling.Ignore)] + [InlineData("true", NullValueHandling.Include, true, NullValueHandling.Include)] + public void Parse_QueryConfigWithApiSettings_Succeeds(string queryValue, NullValueHandling defaultValue, bool allowOverride, NullValueHandling expected) { // Arrange - var query = new KeyValuePair("nulls", queryValue); + const string parameterName = "nulls"; var service = GetService(defaultValue, allowOverride); // Act - if (service.CanParse(query.Key) && service.IsEnabled(DisableQueryAttribute.Empty)) + if (service.CanParse(parameterName) && service.IsEnabled(DisableQueryAttribute.Empty)) { - service.Parse(query.Key, query.Value); + service.Parse(parameterName, queryValue); } // Assert - Assert.Equal(expected, service.OmitAttributeIfValueIsNull); + Assert.Equal(expected, service.SerializerNullValueHandling); } [Fact] @@ -78,7 +77,7 @@ public void Parse_NullsService_FailOnNonBooleanValue() { // Arrange const string parameterName = "nulls"; - var service = GetService(true, true); + var service = GetService(NullValueHandling.Include, true); // Act, assert var exception = Assert.Throws(() => service.Parse(parameterName, "some")); From 1d6eaf8f8febfc200810b69bdf3ac352eeda661a Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 10 Apr 2020 18:02:31 +0200 Subject: [PATCH 09/11] Added test to see serializer override being used in output --- .../JsonApiDotNetCoreExample/Models/Gender.cs | 9 ++ .../JsonApiDotNetCoreExample/Models/Person.cs | 3 + .../Startups/Startup.cs | 2 + .../Acceptance/SerializationTests.cs | 131 ++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 src/Examples/JsonApiDotNetCoreExample/Models/Gender.cs create mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Gender.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Gender.cs new file mode 100644 index 0000000000..4990932a0a --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Gender.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCoreExample.Models +{ + public enum Gender + { + Unknown, + Male, + Female + } +} diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs index 386c2029ba..94090b9c22 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -23,6 +23,9 @@ public sealed class Person : Identifiable, IIsLockable [Attr("the-Age")] public int Age { get; set; } + [Attr] + public Gender Gender { get; set; } + [HasMany] public List TodoItems { get; set; } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs index e79e31d018..7946332764 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -9,6 +9,7 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Query; using JsonApiDotNetCoreExample.Services; +using Newtonsoft.Json.Converters; namespace JsonApiDotNetCoreExample { @@ -52,6 +53,7 @@ protected virtual void ConfigureJsonApiOptions(JsonApiOptions options) options.IncludeTotalRecordCount = true; options.LoadDatabaseValues = true; options.ValidateModelState = true; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); } public void Configure( diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs new file mode 100644 index 0000000000..1a8eae7521 --- /dev/null +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/SerializationTests.cs @@ -0,0 +1,131 @@ +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models.JsonApiDocuments; +using JsonApiDotNetCoreExample.Data; +using JsonApiDotNetCoreExample.Models; +using JsonApiDotNetCoreExampleTests.Acceptance.Spec; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace JsonApiDotNetCoreExampleTests.Acceptance +{ + public sealed class SerializationTests : FunctionalTestCollection + { + public SerializationTests(StandardApplicationFactory factory) + : base(factory) + { + } + + [Fact] + public async Task When_getting_person_it_must_match_JSON_text() + { + // Arrange + var person = new Person + { + Id = 123, + FirstName = "John", + LastName = "Doe", + Age = 57, + Gender = Gender.Male + }; + + _dbContext.People.RemoveRange(_dbContext.People); + _dbContext.People.Add(person); + _dbContext.SaveChanges(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/api/v1/people/" + person.Id); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var bodyText = await response.Content.ReadAsStringAsync(); + var token = JsonConvert.DeserializeObject(bodyText); + var bodyFormatted = token.ToString().Replace("\r\n", "\n"); + + Assert.Equal(@"{ + ""meta"": { + ""copyright"": ""Copyright 2015 Example Corp."", + ""authors"": [ + ""Jared Nance"", + ""Maurits Moeys"", + ""Harro van der Kroft"" + ] + }, + ""links"": { + ""self"": ""http://localhost/api/v1/people/123"" + }, + ""data"": { + ""type"": ""people"", + ""id"": ""123"", + ""attributes"": { + ""firstName"": ""John"", + ""lastName"": ""Doe"", + ""the-Age"": 57, + ""gender"": ""Male"" + }, + ""relationships"": { + ""todoItems"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/todoItems"", + ""related"": ""http://localhost/api/v1/people/123/todoItems"" + } + }, + ""assignedTodoItems"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/assignedTodoItems"", + ""related"": ""http://localhost/api/v1/people/123/assignedTodoItems"" + } + }, + ""todoCollections"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/todoCollections"", + ""related"": ""http://localhost/api/v1/people/123/todoCollections"" + } + }, + ""role"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/role"", + ""related"": ""http://localhost/api/v1/people/123/role"" + } + }, + ""oneToOneTodoItem"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/oneToOneTodoItem"", + ""related"": ""http://localhost/api/v1/people/123/oneToOneTodoItem"" + } + }, + ""stakeHolderTodoItem"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/stakeHolderTodoItem"", + ""related"": ""http://localhost/api/v1/people/123/stakeHolderTodoItem"" + } + }, + ""unIncludeableItem"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/unIncludeableItem"", + ""related"": ""http://localhost/api/v1/people/123/unIncludeableItem"" + } + }, + ""passport"": { + ""links"": { + ""self"": ""http://localhost/api/v1/people/123/relationships/passport"", + ""related"": ""http://localhost/api/v1/people/123/passport"" + } + } + }, + ""links"": { + ""self"": ""http://localhost/api/v1/people/123"" + } + } +}", bodyFormatted); + } + } +} From b52b5b4597f2757e5fb868889f60f923ad8c05f7 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Fri, 10 Apr 2020 22:43:42 +0200 Subject: [PATCH 10/11] Minor serialization fixes --- .../Extensions/JsonSerializerExtensions.cs | 11 +++++++++++ .../Serialization/Client/RequestSerializer.cs | 9 +++++---- .../Extensibility/NoEntityFrameworkTests.cs | 4 ++-- test/NoEntityFrameworkTests/TestFixture.cs | 2 +- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs index eba2b8f65f..38dde88f63 100644 --- a/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs @@ -35,6 +35,17 @@ public override string GetExtensionDataName(string name) return ResolvePropertyName(name); } + public override string GetDictionaryKey(string key) + { + // Ignore the value of ProcessDictionaryKeys property on the wrapped strategy (short-circuit). + return ResolvePropertyName(key); + } + + public override string GetPropertyName(string name, bool hasSpecifiedName) + { + return _namingStrategy.GetPropertyName(name, hasSpecifiedName); + } + protected override string ResolvePropertyName(string name) { return _namingStrategy.GetPropertyName(name, false); diff --git a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs index ad2d470447..1906469b03 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs @@ -15,6 +15,7 @@ public class RequestSerializer : BaseDocumentBuilder, IRequestSerializer { private Type _currentTargetedResource; private readonly IResourceGraph _resourceGraph; + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings(); public RequestSerializer(IResourceGraph resourceGraph, IResourceObjectBuilder resourceObjectBuilder) @@ -29,14 +30,14 @@ public string Serialize(IIdentifiable entity) if (entity == null) { var empty = Build((IIdentifiable) null, new List(), new List()); - return SerializeObject(empty, new JsonSerializerSettings()); + return SerializeObject(empty, _jsonSerializerSettings); } _currentTargetedResource = entity.GetType(); var document = Build(entity, GetAttributesToSerialize(entity), GetRelationshipsToSerialize(entity)); _currentTargetedResource = null; - return SerializeObject(document, new JsonSerializerSettings()); + return SerializeObject(document, _jsonSerializerSettings); } /// @@ -52,7 +53,7 @@ public string Serialize(IEnumerable entities) if (entity == null) { var result = Build(entities, new List(), new List()); - return SerializeObject(result, new JsonSerializerSettings()); + return SerializeObject(result, _jsonSerializerSettings); } _currentTargetedResource = entity.GetType(); @@ -60,7 +61,7 @@ public string Serialize(IEnumerable entities) var relationships = GetRelationshipsToSerialize(entity); var document = Build(entities, attributes, relationships); _currentTargetedResource = null; - return SerializeObject(document, new JsonSerializerSettings()); + return SerializeObject(document, _jsonSerializerSettings); } /// diff --git a/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs b/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs index ca21468d76..a71495c367 100644 --- a/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs +++ b/test/NoEntityFrameworkTests/Acceptance/Extensibility/NoEntityFrameworkTests.cs @@ -6,9 +6,9 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Newtonsoft.Json; +using NoEntityFrameworkExample; +using NoEntityFrameworkExample.Models; using Xunit; -using Startup = NoEntityFrameworkExample.Startup; -using TodoItem = NoEntityFrameworkExample.Models.TodoItem; namespace NoEntityFrameworkTests.Acceptance.Extensibility { diff --git a/test/NoEntityFrameworkTests/TestFixture.cs b/test/NoEntityFrameworkTests/TestFixture.cs index fc6d6bc0df..e145ec3ba5 100644 --- a/test/NoEntityFrameworkTests/TestFixture.cs +++ b/test/NoEntityFrameworkTests/TestFixture.cs @@ -9,7 +9,7 @@ using System; using System.Linq.Expressions; using JsonApiDotNetCore.Configuration; -using Startup = NoEntityFrameworkExample.Startup; +using NoEntityFrameworkExample; namespace NoEntityFrameworkTests { From 3c35def5afba6a23f6ebba39040d5c80c7edcc27 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Mon, 13 Apr 2020 11:54:43 +0200 Subject: [PATCH 11/11] Removed commented-out files --- .../DocumentBuilderBehaviour_Tests.cs | 72 ------------------ test/UnitTests/Builders/MetaBuilderTests.cs | 75 ------------------- 2 files changed, 147 deletions(-) delete mode 100644 test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs delete mode 100644 test/UnitTests/Builders/MetaBuilderTests.cs diff --git a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs b/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs deleted file mode 100644 index 20695c89c5..0000000000 --- a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs +++ /dev/null @@ -1,72 +0,0 @@ -// TODO: Why is this file commented out? - -//using JsonApiDotNetCore.Builders; -//using JsonApiDotNetCore.Configuration; -//using JsonApiDotNetCore.Services; -//using Microsoft.AspNetCore.Http; -//using Moq; -//using Xunit; - -//namespace UnitTests.Builders -//{ -// public class BaseDocumentBuilderBehaviour_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 BaseDocumentBuilderOptionsProvider(jsonApiContextMock.Object, httpContextAccessorMock.Object); -// var documentBuilderOptions = sut.GetBaseDocumentBuilderOptions(); - -// Assert.Equal(omitsNulls, documentBuilderOptions.OmitNullValuedAttributes); -// } - -// } -//} diff --git a/test/UnitTests/Builders/MetaBuilderTests.cs b/test/UnitTests/Builders/MetaBuilderTests.cs deleted file mode 100644 index a691f2d387..0000000000 --- a/test/UnitTests/Builders/MetaBuilderTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// TODO: Why is this file commented out? - -//using System.Collections.Generic; -//using JsonApiDotNetCore.Builders; -//using Xunit; - -//namespace UnitTests.Builders -//{ -// public class MetaBuilderTests -// { -// [Fact] -// public void Can_Add_Key_Value() -// { -// // Arrange -// var builder = new MetaBuilder(); -// var key = "test"; -// var value = "testValue"; - -// // Act -// builder.Add(key, value); -// var result = builder.Build(); - -// // Assert -// Assert.NotEmpty(result); -// Assert.Equal(value, result[key]); -// } - -// [Fact] -// public void Can_Add_Multiple_Values() -// { -// // Arrange -// var builder = new MetaBuilder(); -// var input = new Dictionary { -// { "key1", "value1" }, -// { "key2", "value2" } -// }; - -// // Act -// builder.Add(input); -// var result = builder.Build(); - -// // Assert -// Assert.NotEmpty(result); -// foreach (var entry in input) -// Assert.Equal(input[entry.Key], result[entry.Key]); -// } - -// [Fact] -// public void When_Adding_Duplicate_Values_Keep_Newest() -// { -// // Arrange -// var builder = new MetaBuilder(); - -// var key = "key"; -// var oldValue = "oldValue"; -// var newValue = "newValue"; - -// builder.Add(key, oldValue); - -// var input = new Dictionary { -// { key, newValue }, -// { "key2", "value2" } -// }; - -// // Act -// builder.Add(input); -// var result = builder.Build(); - -// // Assert -// Assert.NotEmpty(result); -// Assert.Equal(input.Count, result.Count); -// Assert.Equal(input[key], result[key]); -// } -// } -//}