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..79b9caabf1 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))); @@ -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/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 5493c3d54f..c9babb5d76 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; @@ -24,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>(); @@ -34,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()); + includeBuilderMock.Object, fieldsToSerialize, resourceObjectBuilder, options); } private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph) 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 58e2c8c67b..94090b9c22 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs @@ -20,9 +20,12 @@ public sealed class Person : Identifiable, IIsLockable [Attr] public string LastName { get; set; } - [Attr] + [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/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 d0f0fa73ff..ac26cbf82b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs @@ -6,8 +6,10 @@ using Microsoft.EntityFrameworkCore; using JsonApiDotNetCore.Extensions; using System; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Query; using JsonApiDotNetCoreExample.Services; +using Newtonsoft.Json.Converters; namespace JsonApiDotNetCoreExample { @@ -37,21 +39,24 @@ 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; - options.EnableResourceHooks = 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; + options.EnableResourceHooks = true; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + } + 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..8cbe4eba4a 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(); @@ -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() @@ -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/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 c20e2eaf7f..cf886b6730 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,30 @@ 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 using a query string parameter. + /// + bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } + + /// + /// Determines whether the serialization setting can be overridden by using a query string parameter. + /// + bool AllowQueryStringOverrideForSerializerDefaultValueHandling { 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 a7a7edda7b..d7bc3d4bd9 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 { @@ -51,6 +52,12 @@ public class JsonApiOptions : IJsonApiOptions /// public string Namespace { get; set; } + /// + public bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } + + /// + public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; } + /// /// The default page size for all resources. The value zero means: no paging. /// @@ -106,16 +113,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. /// @@ -126,9 +123,13 @@ public class JsonApiOptions : IJsonApiOptions /// public bool ValidateModelState { get; set; } + /// public JsonSerializerSettings SerializerSettings { get; } = new JsonSerializerSettings { - NullValueHandling = NullValueHandling.Ignore + ContractResolver = new DefaultContractResolver + { + NamingStrategy = new CamelCaseNamingStrategy() + } }; } } 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/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 new file mode 100644 index 0000000000..38dde88f63 --- /dev/null +++ b/src/JsonApiDotNetCore/Extensions/JsonSerializerExtensions.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.Extensions +{ + internal static class JsonSerializerExtensions + { + public static void ApplyErrorSettings(this JsonSerializer jsonSerializer) + { + jsonSerializer.NullValueHandling = NullValueHandling.Ignore; + + // 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); + } + + 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/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/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/Middleware/CurrentRequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs index ff19a00e97..9b6f885b15 100644 --- a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs @@ -4,6 +4,7 @@ using System.Net; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers.Contracts; @@ -135,7 +136,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 +152,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 +195,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(); + + // 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/QueryParameterServices/Contracts/IOmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs similarity index 63% rename from src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs index 1b21ee8b37..0d69326296 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IDefaultsService.cs @@ -1,13 +1,15 @@ +using Newtonsoft.Json; + 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 ?defaults=false /// - public interface IOmitNullService : IQueryParameterService + public interface IDefaultsService : IQueryParameterService { /// /// Contains the effective value of default configuration and query string override, after parsing has occured. /// - bool OmitAttributeIfValueIsNull { get; } + DefaultValueHandling SerializerDefaultValueHandling { get; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs similarity index 65% rename from src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs index ec8213fdb7..038eaa7153 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/INullsService.cs @@ -1,13 +1,15 @@ +using Newtonsoft.Json; + 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 ?nulls=false /// - public interface IOmitDefaultService : IQueryParameterService + public interface INullsService : IQueryParameterService { /// /// Contains the effective value of default configuration and query string override, after parsing has occured. /// - bool OmitAttributeIfValueIsDefault { get; } + NullValueHandling SerializerNullValueHandling { get; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs similarity index 59% rename from src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs index 80d26127d4..aa8c597884 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/DefaultsService.cs @@ -2,46 +2,48 @@ using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Exceptions; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; 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.DefaultAttributeResponseBehavior.OmitAttributeIfValueIsDefault; + 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.DefaultAttributeResponseBehavior.AllowQueryStringOverride && - !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; + SerializerDefaultValueHandling = result ? DefaultValueHandling.Include : DefaultValueHandling.Ignore; } } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs similarity index 62% rename from src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs index 3fb26decf5..df3f06a14e 100644 --- a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/NullsService.cs @@ -2,47 +2,48 @@ using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Exceptions; using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; 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.NullAttributeResponseBehavior.OmitAttributeIfValueIsNull; + SerializerNullValueHandling = options.SerializerSettings.NullValueHandling; _options = options; } /// - public bool OmitAttributeIfValueIsNull { get; private set; } + public NullValueHandling SerializerNullValueHandling { get; private set; } /// public bool IsEnabled(DisableQueryAttribute disableQueryAttribute) { - return _options.NullAttributeResponseBehavior.AllowQueryStringOverride && - !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; + SerializerNullValueHandling = result ? NullValueHandling.Include : NullValueHandling.Ignore; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/RequestSerializer.cs index 94fd06cb39..1906469b03 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; @@ -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) @@ -27,12 +28,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, _jsonSerializerSettings); + } _currentTargetedResource = entity.GetType(); var document = Build(entity, GetAttributesToSerialize(entity), GetRelationshipsToSerialize(entity)); _currentTargetedResource = null; - return JsonConvert.SerializeObject(document); + + return SerializeObject(document, _jsonSerializerSettings); } /// @@ -44,15 +49,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, _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, _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/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 e758366040..2eceb3809d 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilderSettings.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Models; +using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -6,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 d75e7bde98..60f88112d3 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs @@ -9,20 +9,19 @@ 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.SerializerNullValueHandling, _defaultsService.SerializerDefaultValueHandling); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs index 3891e31cd0..720f782f49 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResponseSerializer.cs @@ -1,17 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; -using JsonApiDotNetCore.Graph; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Extensions; 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 { - /// /// Server serializer implementation of /// @@ -30,34 +29,26 @@ public class ResponseSerializer : BaseDocumentBuilder, IJsonApiSerial private readonly Dictionary> _attributesToSerializeCache = new Dictionary>(); private readonly Dictionary> _relationshipsToSerializeCache = new Dictionary>(); private readonly IFieldsToSerialize _fieldsToSerialize; + private readonly IJsonApiOptions _options; private readonly IMetaBuilder _metaBuilder; private readonly Type _primaryResourceType; private readonly ILinkBuilder _linkBuilder; private readonly IIncludedResourceObjectBuilder _includedBuilder; - private readonly JsonSerializerSettings _errorSerializerSettings; public ResponseSerializer(IMetaBuilder metaBuilder, ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, IFieldsToSerialize fieldsToSerialize, IResourceObjectBuilder resourceObjectBuilder, - IResourceNameFormatter formatter) + IJsonApiOptions options) : base(resourceObjectBuilder) { _fieldsToSerialize = fieldsToSerialize; + _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 +63,7 @@ public string Serialize(object data) private string SerializeErrorDocument(ErrorDocument errorDocument) { - return JsonConvert.SerializeObject(errorDocument, _errorSerializerSettings); + return SerializeObject(errorDocument, _options.SerializerSettings, serializer => { serializer.ApplyErrorSettings(); }); } /// @@ -84,7 +75,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 +87,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,12 +116,13 @@ internal string SerializeMany(IEnumerable entities) } AddTopLevelObjects(document); - return JsonConvert.SerializeObject(document); + + return SerializeObject(document, _options.SerializerSettings, serializer => { serializer.NullValueHandling = NullValueHandling.Include; }); } /// /// 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 @@ -176,23 +171,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/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/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 ec98d103fd..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/OmitAttributeIfValueIsNullTests.cs +++ /dev/null @@ -1,143 +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 Newtonsoft.Json; -using Xunit; - -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 - { - Description = null, - Ordinal = 1, - CreatedDate = DateTime.Now, - AchievedDate = DateTime.Now.AddDays(2), - Owner = person - }; - _todoItem = _dbContext.TodoItems.Add(_todoItem).Entity; - } - - public async Task InitializeAsync() - { - await _dbContext.SaveChangesAsync(); - } - - public Task DisposeAsync() - { - return Task.CompletedTask; - } - - [Theory] - [InlineData(null, null, null, false)] - [InlineData(true, null, null, true)] - [InlineData(false, true, "true", true)] - [InlineData(false, false, "true", false)] - [InlineData(true, true, "false", false)] - [InlineData(true, false, "false", true)] - [InlineData(null, false, "false", false)] - [InlineData(null, false, "true", false)] - [InlineData(null, true, "true", true)] - [InlineData(null, true, "false", false)] - [InlineData(null, true, "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) - { - - // 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; - - var httpMethod = new HttpMethod("GET"); - var queryString = allowQueryStringOverride.HasValue - ? $"&omitNull={queryStringOverride}" - : ""; - var route = $"/api/v1/todoItems/{_todoItem.Id}?include=owner{queryString}"; - var request = new HttpRequestMessage(httpMethod, route); - - // Act - var response = await _fixture.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 'omitNull' cannot be used at this endpoint.", errorDocument.Errors[0].Detail); - Assert.Equal("omitNull", 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 'omitNull' query string parameter.", errorDocument.Errors[0].Detail); - Assert.Equal("omitNull", 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 'omitNull' is not a valid boolean value.", errorDocument.Errors[0].Detail); - Assert.Equal("omitNull", 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/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); + } + } +} 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/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..2fe4565bd1 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() @@ -167,9 +170,10 @@ 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 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); @@ -209,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 @@ -224,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/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/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 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/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 5196a1d7a8..e145ec3ba5 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,7 +8,8 @@ using NoEntityFrameworkExample.Models; using System; using System.Linq.Expressions; -using Startup = NoEntityFrameworkExample.Startup; +using JsonApiDotNetCore.Configuration; +using NoEntityFrameworkExample; 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 deleted file mode 100644 index ce6d4bf742..0000000000 --- a/test/UnitTests/Builders/DocumentBuilderBehaviour_Tests.cs +++ /dev/null @@ -1,70 +0,0 @@ -//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 58e35b7bd6..0000000000 --- a/test/UnitTests/Builders/MetaBuilderTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -//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]); -// } -// } -//} 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/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/DefaultsServiceTests.cs b/test/UnitTests/QueryParameters/DefaultsServiceTests.cs new file mode 100644 index 0000000000..a324427fa9 --- /dev/null +++ b/test/UnitTests/QueryParameters/DefaultsServiceTests.cs @@ -0,0 +1,92 @@ +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Query; +using Newtonsoft.Json; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public sealed class DefaultsServiceTests : QueryParametersUnitTestCollection + { + public DefaultsService GetService(DefaultValueHandling defaultValue, bool allowOverride) + { + var options = new JsonApiOptions + { + SerializerSettings = { DefaultValueHandling = defaultValue }, + AllowQueryStringOverrideForSerializerDefaultValueHandling = allowOverride + }; + + return new DefaultsService(options); + } + + [Fact] + public void CanParse_DefaultsService_SucceedOnMatch() + { + // Arrange + var service = GetService(DefaultValueHandling.Include, true); + + // Act + bool result = service.CanParse("defaults"); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanParse_DefaultsService_FailOnMismatch() + { + // Arrange + var service = GetService(DefaultValueHandling.Include, true); + + // Act + bool result = service.CanParse("defaultsettings"); + + // Assert + Assert.False(result); + } + + [Theory] + [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 + const string parameterName = "defaults"; + var service = GetService(defaultValue, allowOverride); + + // Act + if (service.CanParse(parameterName) && service.IsEnabled(DisableQueryAttribute.Empty)) + { + service.Parse(parameterName, queryValue); + } + + // Assert + Assert.Equal(expected, service.SerializerDefaultValueHandling); + } + + [Fact] + public void Parse_DefaultsService_FailOnNonBooleanValue() + { + // Arrange + const string parameterName = "defaults"; + var service = GetService(DefaultValueHandling.Include, true); + + // Act, assert + var exception = Assert.Throws(() => service.Parse(parameterName, "some")); + + Assert.Equal(parameterName, exception.QueryParameterName); + Assert.Equal(HttpStatusCode.BadRequest, exception.Error.StatusCode); + Assert.Equal("The specified query string value must be 'true' or 'false'.", exception.Error.Title); + Assert.Equal($"The value 'some' for parameter '{parameterName}' is not a valid boolean value.", exception.Error.Detail); + Assert.Equal(parameterName, exception.Error.Source.Parameter); + } + } +} diff --git a/test/UnitTests/QueryParameters/NullsServiceTests.cs b/test/UnitTests/QueryParameters/NullsServiceTests.cs new file mode 100644 index 0000000000..8b65752918 --- /dev/null +++ b/test/UnitTests/QueryParameters/NullsServiceTests.cs @@ -0,0 +1,92 @@ +using System.Net; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Exceptions; +using JsonApiDotNetCore.Query; +using Newtonsoft.Json; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public sealed class NullsServiceTests : QueryParametersUnitTestCollection + { + public NullsService GetService(NullValueHandling defaultValue, bool allowOverride) + { + var options = new JsonApiOptions + { + SerializerSettings = { NullValueHandling = defaultValue }, + AllowQueryStringOverrideForSerializerNullValueHandling = allowOverride + }; + + return new NullsService(options); + } + + [Fact] + public void CanParse_NullsService_SucceedOnMatch() + { + // Arrange + var service = GetService(NullValueHandling.Include, true); + + // Act + bool result = service.CanParse("nulls"); + + // Assert + Assert.True(result); + } + + [Fact] + public void CanParse_NullsService_FailOnMismatch() + { + // Arrange + var service = GetService(NullValueHandling.Include, true); + + // Act + bool result = service.CanParse("nullsettings"); + + // Assert + Assert.False(result); + } + + [Theory] + [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 + const string parameterName = "nulls"; + var service = GetService(defaultValue, allowOverride); + + // Act + if (service.CanParse(parameterName) && service.IsEnabled(DisableQueryAttribute.Empty)) + { + service.Parse(parameterName, queryValue); + } + + // Assert + Assert.Equal(expected, service.SerializerNullValueHandling); + } + + [Fact] + public void Parse_NullsService_FailOnNonBooleanValue() + { + // Arrange + const string parameterName = "nulls"; + var service = GetService(NullValueHandling.Include, true); + + // Act, assert + var exception = Assert.Throws(() => service.Parse(parameterName, "some")); + + Assert.Equal(parameterName, exception.QueryParameterName); + Assert.Equal(HttpStatusCode.BadRequest, exception.Error.StatusCode); + Assert.Equal("The specified query string value must be 'true' or 'false'.", exception.Error.Title); + Assert.Equal($"The value 'some' for parameter '{parameterName}' is not a valid boolean value.", exception.Error.Detail); + Assert.Equal(parameterName, exception.Error.Source.Parameter); + } + } +} diff --git a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs b/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs deleted file mode 100644 index 75ee3517ff..0000000000 --- a/test/UnitTests/QueryParameters/OmitDefaultServiceTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Query; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace UnitTests.QueryParameters -{ - public sealed class OmitDefaultServiceTests : QueryParametersUnitTestCollection - { - public OmitDefaultService GetService(bool @default, bool @override) - { - var options = new JsonApiOptions - { - DefaultAttributeResponseBehavior = new DefaultAttributeResponseBehavior(@default, @override) - }; - - return new OmitDefaultService(options); - } - - [Fact] - public void CanParse_OmitDefaultService_SucceedOnMatch() - { - // Arrange - var service = GetService(true, true); - - // Act - bool result = service.CanParse("omitDefault"); - - // Assert - Assert.True(result); - } - - [Fact] - public void CanParse_OmitDefaultService_FailOnMismatch() - { - // Arrange - var service = GetService(true, true); - - // Act - bool result = service.CanParse("omit-default"); - - // 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) - { - // Arrange - var query = new KeyValuePair("omitDefault", queryValue); - var service = GetService(@default, @override); - - // Act - if (service.CanParse(query.Key) && service.IsEnabled(DisableQueryAttribute.Empty)) - { - service.Parse(query.Key, query.Value); - } - - // Assert - Assert.Equal(expected, service.OmitAttributeIfValueIsDefault); - } - - [Fact] - public void Parse_OmitDefaultService_FailOnNonBooleanValue() - { - // Arrange - const string parameterName = "omit-default"; - var service = GetService(true, true); - - // Act, assert - var exception = Assert.Throws(() => service.Parse(parameterName, "some")); - - Assert.Equal(parameterName, exception.QueryParameterName); - Assert.Equal(HttpStatusCode.BadRequest, exception.Error.StatusCode); - Assert.Equal("The specified query string value must be 'true' or 'false'.", exception.Error.Title); - Assert.Equal($"The value 'some' for parameter '{parameterName}' is not a valid boolean value.", exception.Error.Detail); - Assert.Equal(parameterName, exception.Error.Source.Parameter); - } - } -} diff --git a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs b/test/UnitTests/QueryParameters/OmitNullServiceTests.cs deleted file mode 100644 index bb23bdd4b7..0000000000 --- a/test/UnitTests/QueryParameters/OmitNullServiceTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Collections.Generic; -using System.Net; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Exceptions; -using JsonApiDotNetCore.Query; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace UnitTests.QueryParameters -{ - public sealed class OmitNullServiceTests : QueryParametersUnitTestCollection - { - public OmitNullService GetService(bool @default, bool @override) - { - var options = new JsonApiOptions - { - NullAttributeResponseBehavior = new NullAttributeResponseBehavior(@default, @override) - }; - - return new OmitNullService(options); - } - - [Fact] - public void CanParse_OmitNullService_SucceedOnMatch() - { - // Arrange - var service = GetService(true, true); - - // Act - bool result = service.CanParse("omitNull"); - - // Assert - Assert.True(result); - } - - [Fact] - public void CanParse_OmitNullService_FailOnMismatch() - { - // Arrange - var service = GetService(true, true); - - // Act - bool result = service.CanParse("omit-null"); - - // 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) - { - // Arrange - var query = new KeyValuePair("omitNull", queryValue); - var service = GetService(@default, @override); - - // Act - if (service.CanParse(query.Key) && service.IsEnabled(DisableQueryAttribute.Empty)) - { - service.Parse(query.Key, query.Value); - } - - // Assert - Assert.Equal(expected, service.OmitAttributeIfValueIsNull); - } - - [Fact] - public void Parse_OmitNullService_FailOnNonBooleanValue() - { - // Arrange - const string parameterName = "omit-null"; - var service = GetService(true, true); - - // Act, assert - var exception = Assert.Throws(() => service.Parse(parameterName, "some")); - - Assert.Equal(parameterName, exception.QueryParameterName); - Assert.Equal(HttpStatusCode.BadRequest, exception.Error.StatusCode); - Assert.Equal("The specified query string value must be 'true' or 'false'.", exception.Error.Title); - Assert.Equal($"The value 'some' for parameter '{parameterName}' is not a valid boolean value.", exception.Error.Detail); - Assert.Equal(parameterName, exception.Error.Source.Parameter); - } - } -} 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 5796d3fc4c..8bfc24e0fa 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 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]