diff --git a/Directory.Build.props b/Directory.Build.props index baffceb9d2..b5da8620b7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,11 +7,12 @@ 6.2.* 4.2.0 $(MSBuildThisFileDirectory)CodingGuidelines.ruleset + 9999 - + @@ -23,11 +24,11 @@ - 33.0.2 + 33.1.1 3.1.0 - 5.10.3 + 6.1.0 4.16.1 2.4.* - 16.10.0 + 16.11.0 diff --git a/ROADMAP.md b/ROADMAP.md index ad6d1f0418..1c15a33b2b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -19,8 +19,8 @@ The need for breaking changes has blocked several efforts in the v4.x release, s - [x] Tweak trace logging [#1033](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1033) - [x] Instrumentation [#1032](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1032) - [x] Optimized delete to-many [#1030](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1030) -- [ ] Support System.Text.Json [#664](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/664) [#999](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/999) [#233](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/233) -- [ ] Optimize IIdentifiable to ResourceObject conversion [#1028](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1028) [#1024](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1024) +- [x] Support System.Text.Json [#664](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/664) [#999](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/999) [1077](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1077) [1078](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1078) +- [ ] Optimize IIdentifiable to ResourceObject conversion [#1028](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1028) [#1024](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1024) [#233](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/233) - [ ] Nullable reference types [#1029](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1029) Aside from the list above, we have interest in the following topics. It's too soon yet to decide whether they'll make it into v5.x or in a later major version. diff --git a/appveyor.yml b/appveyor.yml index e9f32b3553..8bddadbc32 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -35,10 +35,7 @@ for: only: - image: Visual Studio 2019 services: - - postgresql134 - # https://help.appveyor.com/discussions/problems/30239-postgres-fails-to-connect-after-version-change - init: - - net start postgresql-x64-13 + - postgresql13 # REF: https://github.com/docascode/docfx-seed/blob/master/appveyor.yml before_build: - pwsh: | diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs index 7ecbafffbc..184ba5a082 100644 --- a/benchmarks/DependencyFactory.cs +++ b/benchmarks/DependencyFactory.cs @@ -8,7 +8,10 @@ internal sealed class DependencyFactory public IResourceGraph CreateResourceGraph(IJsonApiOptions options) { var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); + builder.Add(BenchmarkResourcePublicNames.Type); + builder.Add(); + return builder.Build(); } } diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs index fd791f5d8a..8f1ec950da 100644 --- a/benchmarks/Query/QueryParserBenchmarks.cs +++ b/benchmarks/Query/QueryParserBenchmarks.cs @@ -64,11 +64,9 @@ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGr var sortReader = new SortQueryStringParameterReader(request, resourceGraph); var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(request, resourceGraph); var paginationReader = new PaginationQueryStringParameterReader(request, resourceGraph, options); - var defaultsReader = new DefaultsQueryStringParameterReader(options); - var nullsReader = new NullsQueryStringParameterReader(options); IQueryStringParameterReader[] readers = ArrayFactory.Create(includeReader, filterReader, sortReader, - sparseFieldSetReader, paginationReader, defaultsReader, nullsReader); + sparseFieldSetReader, paginationReader); return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance); } diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index 6e3bcf2b61..2c2cb62223 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -1,14 +1,11 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.Design; +using System.Text.Json; using BenchmarkDotNet.Attributes; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; -using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; namespace Benchmarks.Serialization { @@ -16,15 +13,14 @@ namespace Benchmarks.Serialization [MarkdownExporter] public class JsonApiDeserializerBenchmarks { - private static readonly string Content = JsonConvert.SerializeObject(new Document + private static readonly string RequestBody = JsonSerializer.Serialize(new { - Data = new ResourceObject + data = new { - Type = BenchmarkResourcePublicNames.Type, - Id = "1", - Attributes = new Dictionary + type = BenchmarkResourcePublicNames.Type, + id = "1", + attributes = new { - ["name"] = Guid.NewGuid().ToString() } } }); @@ -55,7 +51,7 @@ public JsonApiDeserializerBenchmarks() [Benchmark] public object DeserializeSimpleObject() { - return _jsonApiDeserializer.Deserialize(Content); + return _jsonApiDeserializer.Deserialize(RequestBody); } } } diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs index ffbebc2cfc..0fa58c272e 100644 --- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs @@ -34,7 +34,7 @@ public JsonApiSerializerBenchmarks() ILinkBuilder linkBuilder = new Mock().Object; IIncludedResourceObjectBuilder includeBuilder = new Mock().Object; - var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings()); + var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, options); IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; diff --git a/docs/request-examples/012_PATCH_Book.ps1 b/docs/request-examples/012_PATCH_Book.ps1 index 080115161c..d704c8c8c8 100644 --- a/docs/request-examples/012_PATCH_Book.ps1 +++ b/docs/request-examples/012_PATCH_Book.ps1 @@ -4,7 +4,7 @@ curl -s -f http://localhost:14141/api/books/1 ` -d '{ \"data\": { \"type\": \"books\", - \"id\": "1", + \"id\": \"1\", \"attributes\": { \"publishYear\": 1820 } diff --git a/docs/usage/options.md b/docs/usage/options.md index 287c1c52f1..e2e099e31e 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -78,27 +78,26 @@ To limit the maximum depth of nested includes, use `MaximumIncludeDepth`. This i options.MaximumIncludeDepth = 1; ``` -## Custom Serializer Settings +## Customize Serializer options -We use [Newtonsoft.Json](https://www.newtonsoft.com/json) for all serialization needs. -If you want to change the default serializer settings, you can: +We use [System.Text.Json](https://www.nuget.org/packages/System.Text.Json) for all serialization needs. +If you want to change the default serializer options, you can: ```c# -options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; -options.SerializerSettings.Converters.Add(new StringEnumConverter()); -options.SerializerSettings.Formatting = Formatting.Indented; +options.SerializerOptions.WriteIndented = true; +options.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve; +options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); ``` The default naming convention (as used in the routes and resource/attribute/relationship names) is also determined here, and can be changed (default is camel-case): ```c# -options.SerializerSettings.ContractResolver = new DefaultContractResolver -{ - NamingStrategy = new KebabCaseNamingStrategy() -}; +// Use Pascal case +options.SerializerOptions.PropertyNamingPolicy = null; +options.SerializerOptions.DictionaryKeyPolicy = null; ``` -Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored. +Because we copy resource properties into an intermediate object before serialization, JSON annotations such as `[JsonPropertyName]` and `[JsonIgnore]` on `[Attr]` properties are ignored. ## Enable ModelState Validation diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index c77c842429..36e424d6e0 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -98,4 +98,4 @@ public class MyModel : Identifiable } ``` -The default naming convention can be changed in [options](~/usage/options.md#custom-serializer-settings). +The default naming convention can be changed in [options](~/usage/options.md#customize-serializer-options). diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index 6e24ab964f..6a42bae7e0 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -14,7 +14,7 @@ public class Person : Identifiable There are two ways the exposed attribute name is determined: -1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings). +1. Using the configured [naming convention](~/usage/options.md#customize-serializer-options). 2. Individually using the attribute's constructor. ```c# @@ -88,9 +88,9 @@ public class Person : Identifiable ## Complex Attributes Models may contain complex attributes. -Serialization of these types is done by [Newtonsoft.Json](https://www.newtonsoft.com/json), -so you should use their APIs to specify serialization formats. -You can also use global options to specify `JsonSerializer` configuration. +Serialization of these types is done by [System.Text.Json](https://www.nuget.org/packages/System.Text.Json), +so you should use their APIs to specify serialization format. +You can also use [global options](~/usage/options.md#customize-serializer-options) to control the `JsonSerializer` behavior. ```c# public class Foo : Identifiable @@ -101,7 +101,8 @@ public class Foo : Identifiable public class Bar { - [JsonProperty("compound-member")] + [JsonPropertyName("compound-member")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string CompoundMember { get; set; } } ``` @@ -121,13 +122,13 @@ public class Foo : Identifiable { get { - return Bar == null ? "{}" : JsonConvert.SerializeObject(Bar); + return Bar == null ? "{}" : JsonSerializer.Serialize(Bar); } set { Bar = string.IsNullOrWhiteSpace(value) ? null - : JsonConvert.DeserializeObject(value); + : JsonSerializer.Deserialize(value); } } } diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 869b38f97c..2495419a6a 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -34,11 +34,37 @@ public class Person : Identifiable The left side of this relationship is of type `Person` (public name: "persons") and the right side is of type `TodoItem` (public name: "todoItems"). +## HasManyThrough + +_removed since v5.0_ + +Earlier versions of Entity Framework Core (up to v5) [did not support](https://github.com/aspnet/EntityFrameworkCore/issues/1368) many-to-many relationships without a join entity. +For this reason, earlier versions of JsonApiDotNetCore filled this gap by allowing applications to declare a relationship as `HasManyThrough`, +which would expose the relationship to the client the same way as any other `HasMany` relationship. +However, under the covers it would use the join type and Entity Framework Core's APIs to get and set the relationship. + +```c# +public class Article : Identifiable +{ + // tells Entity Framework Core to ignore this property + [NotMapped] + + // tells JsonApiDotNetCore to use the join table below + [HasManyThrough(nameof(ArticleTags))] + public ICollection Tags { get; set; } + + // this is the Entity Framework Core navigation to the join table + public ICollection ArticleTags { get; set; } +} +``` + +The left side of this relationship is of type `Article` (public name: "articles") and the right side is of type `Tag` (public name: "tags"). + ## Name There are two ways the exposed relationship name is determined: -1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings). +1. Using the configured [naming convention](~/usage/options.md#customize-serializer-options). 2. Individually using the attribute's constructor. ```c# diff --git a/docs/usage/routing.md b/docs/usage/routing.md index 314e2bdfb1..0a10831d9b 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -45,7 +45,7 @@ The exposed name of the resource ([which can be customized](~/usage/resource-gra ### Non-JSON:API controllers -If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#custom-serializer-settings) is applied to the name of the controller. +If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#customize-serializer-options) is applied to the name of the controller. ```c# public class OrderLineController : ControllerBase diff --git a/src/Examples/GettingStarted/Properties/launchSettings.json b/src/Examples/GettingStarted/Properties/launchSettings.json index b68c2481ed..bcf154605c 100644 --- a/src/Examples/GettingStarted/Properties/launchSettings.json +++ b/src/Examples/GettingStarted/Properties/launchSettings.json @@ -10,7 +10,7 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "api/people", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" @@ -18,7 +18,7 @@ }, "Kestrel": { "commandName": "Project", - "launchBrowser": false, + "launchBrowser": true, "launchUrl": "api/people", "applicationUrl": "http://localhost:14141", "environmentVariables": { diff --git a/src/Examples/GettingStarted/Startup.cs b/src/Examples/GettingStarted/Startup.cs index b942ecc98d..10d2f338f0 100644 --- a/src/Examples/GettingStarted/Startup.cs +++ b/src/Examples/GettingStarted/Startup.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; namespace GettingStarted { @@ -21,7 +20,7 @@ public void ConfigureServices(IServiceCollection services) options.Namespace = "api"; options.UseRelativeLinks = true; options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerOptions.WriteIndented = true; }); } diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs index 49708c5465..b26b82d27d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs @@ -28,21 +28,21 @@ public async Task PostAsync() return BadRequest("Please send your name."); } - string result = "Hello, " + name; + string result = $"Hello, {name}"; return Ok(result); } [HttpPut] public IActionResult Put([FromBody] string name) { - string result = "Hi, " + name; + string result = $"Hi, {name}"; return Ok(result); } [HttpPatch] public IActionResult Patch(string name) { - string result = "Good day, " + name; + string result = $"Good day, {name}"; return Ok(result); } diff --git a/src/Examples/JsonApiDotNetCoreExample/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startup.cs index 570def00e4..e90f81a0f5 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Startup.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json.Serialization; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.OpenApi; @@ -10,8 +11,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace JsonApiDotNetCoreExample { @@ -55,8 +54,8 @@ public void ConfigureServices(IServiceCollection services) options.UseRelativeLinks = true; options.ValidateModelState = true; options.IncludeTotalResourceCount = true; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); + options.SerializerOptions.WriteIndented = true; + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); #if DEBUG options.IncludeExceptionStackTraceInErrors = true; #endif diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj index 251e13bd19..ab4a38c537 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiDotNetCore.OpenApi.csproj @@ -28,6 +28,5 @@ - diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index 36d6dc6d06..8e45c93397 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using Humanizer; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; @@ -9,7 +10,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.OpenApi { @@ -35,17 +35,17 @@ internal sealed class JsonApiOperationIdSelector }; private readonly IControllerResourceMapping _controllerResourceMapping; - private readonly NamingStrategy _namingStrategy; + private readonly JsonNamingPolicy _namingPolicy; private readonly ResourceNameFormatter _formatter; - public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, NamingStrategy namingStrategy) + public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, JsonNamingPolicy namingPolicy) { ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); - ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); + ArgumentGuard.NotNull(namingPolicy, nameof(namingPolicy)); _controllerResourceMapping = controllerResourceMapping; - _namingStrategy = namingStrategy; - _formatter = new ResourceNameFormatter(namingStrategy); + _namingPolicy = namingPolicy; + _formatter = new ResourceNameFormatter(namingPolicy); } public string GetOperationId(ApiDescription endpoint) @@ -109,7 +109,7 @@ private string ApplyTemplate(string operationIdTemplate, Type primaryResourceTyp // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - return _namingStrategy.GetPropertyName(pascalCaseId, false); + return _namingPolicy.ConvertName(pascalCaseId); } } } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index a9dac443b0..e87497c938 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -31,22 +31,22 @@ internal sealed class JsonApiSchemaIdSelector }; private readonly ResourceNameFormatter _formatter; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public JsonApiSchemaIdSelector(ResourceNameFormatter formatter, IResourceContextProvider resourceContextProvider) + public JsonApiSchemaIdSelector(ResourceNameFormatter formatter, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(formatter, nameof(formatter)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _formatter = formatter; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public string GetSchemaId(Type type) { ArgumentGuard.NotNull(type, nameof(type)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(type); + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(type); if (resourceContext != null) { diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs index 89829d5d25..5f0f36d56a 100644 --- a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs @@ -16,16 +16,16 @@ namespace JsonApiDotNetCore.OpenApi /// internal sealed class OpenApiEndpointConvention : IActionModelConvention { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IControllerResourceMapping _controllerResourceMapping; private readonly EndpointResolver _endpointResolver = new(); - public OpenApiEndpointConvention(IResourceContextProvider resourceContextProvider, IControllerResourceMapping controllerResourceMapping) + public OpenApiEndpointConvention(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _controllerResourceMapping = controllerResourceMapping; } @@ -71,7 +71,7 @@ private IReadOnlyCollection GetRelationshipsOfPrimaryReso { Type primaryResourceOfEndpointType = _controllerResourceMapping.GetResourceTypeForController(controllerType); - ResourceContext primaryResourceContext = _resourceContextProvider.GetResourceContext(primaryResourceOfEndpointType); + ResourceContext primaryResourceContext = _resourceGraph.GetResourceContext(primaryResourceOfEndpointType); return primaryResourceContext.Relationships; } diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index f86288113a..855bfee84e 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.OpenApi.SwaggerComponents; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; @@ -57,16 +57,16 @@ private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBu private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection services, Action setupSwaggerGenAction) { var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); - var resourceContextProvider = scope.ServiceProvider.GetRequiredService(); + var resourceGraph = scope.ServiceProvider.GetRequiredService(); var jsonApiOptions = scope.ServiceProvider.GetRequiredService(); - NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; + JsonNamingPolicy namingPolicy = jsonApiOptions.SerializerOptions.PropertyNamingPolicy; AddSchemaGenerator(services); services.AddSwaggerGen(swaggerGenOptions => { - SetOperationInfo(swaggerGenOptions, controllerResourceMapping, resourceContextProvider, namingStrategy); - SetSchemaIdSelector(swaggerGenOptions, resourceContextProvider, namingStrategy); + SetOperationInfo(swaggerGenOptions, controllerResourceMapping, resourceGraph, namingPolicy); + SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy); swaggerGenOptions.DocumentFilter(); setupSwaggerGenAction?.Invoke(swaggerGenOptions); @@ -80,20 +80,20 @@ private static void AddSchemaGenerator(IServiceCollection services) } private static void SetOperationInfo(SwaggerGenOptions swaggerGenOptions, IControllerResourceMapping controllerResourceMapping, - IResourceContextProvider resourceContextProvider, NamingStrategy namingStrategy) + IResourceGraph resourceGraph, JsonNamingPolicy namingPolicy) { - swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping, resourceContextProvider)); + swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping, resourceGraph)); - JsonApiOperationIdSelector jsonApiOperationIdSelector = new(controllerResourceMapping, namingStrategy); + JsonApiOperationIdSelector jsonApiOperationIdSelector = new(controllerResourceMapping, namingPolicy); swaggerGenOptions.CustomOperationIds(jsonApiOperationIdSelector.GetOperationId); } private static IList GetOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping, - IResourceContextProvider resourceContextProvider) + IResourceGraph resourceGraph) { MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod(); Type resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType); - ResourceContext resourceContext = resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = resourceGraph.GetResourceContext(resourceType); return new[] { @@ -101,11 +101,10 @@ private static IList GetOperationTags(ApiDescription description, IContr }; } - private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceContextProvider resourceContextProvider, - NamingStrategy namingStrategy) + private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceGraph resourceGraph, JsonNamingPolicy namingPolicy) { - ResourceNameFormatter resourceNameFormatter = new(namingStrategy); - JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceContextProvider); + ResourceNameFormatter resourceNameFormatter = new(namingPolicy); + JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceGraph); swaggerGenOptions.CustomSchemaIds(type => jsonApiObjectSchemaSelector.GetSchemaId(type)); } @@ -127,10 +126,10 @@ private static void AddSwashbuckleCliCompatibility(IServiceScope scope, IMvcCore private static void AddOpenApiEndpointConvention(IServiceScope scope, IMvcCoreBuilder mvcBuilder) { - var resourceContextProvider = scope.ServiceProvider.GetRequiredService(); + var resourceGraph = scope.ServiceProvider.GetRequiredService(); var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); - mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceContextProvider, controllerResourceMapping))); + mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceGraph, controllerResourceMapping))); } } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index 199c33d2ff..c5723fa0ec 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -5,29 +5,25 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json; -using Swashbuckle.AspNetCore.Newtonsoft; using Swashbuckle.AspNetCore.SwaggerGen; namespace JsonApiDotNetCore.OpenApi.SwaggerComponents { /// - /// For schema generation, we rely on from Swashbuckle for all but our own JSON:API types. + /// For schema generation, we rely on from Swashbuckle for all but our own JSON:API types. /// internal sealed class JsonApiDataContractResolver : ISerializerDataContractResolver { - private readonly NewtonsoftDataContractResolver _dataContractResolver; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly JsonSerializerDataContractResolver _dataContractResolver; + private readonly IResourceGraph _resourceGraph; - public JsonApiDataContractResolver(IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions) + public JsonApiDataContractResolver(IResourceGraph resourceGraph, IJsonApiOptions options) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); - _resourceContextProvider = resourceContextProvider; - - JsonSerializerSettings serializerSettings = jsonApiOptions.SerializerSettings ?? new JsonSerializerSettings(); - _dataContractResolver = new NewtonsoftDataContractResolver(serializerSettings); + _resourceGraph = resourceGraph; + _dataContractResolver = new JsonSerializerDataContractResolver(options.SerializerOptions); } public DataContract GetDataContractForType(Type type) @@ -65,7 +61,7 @@ private static DataContract ReplacePropertiesInDataContract(DataContract dataCon private IList GetDataPropertiesThatExistInResourceContext(Type resourceType, DataContract dataContract) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); var dataProperties = new List(); foreach (DataProperty property in dataContract.ObjectProperties) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index b1589e2004..05e17fabce 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -40,18 +40,16 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator private readonly JsonApiObjectNullabilityProcessor _jsonApiObjectNullabilityProcessor; private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new(); - public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions) + public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); _defaultSchemaGenerator = defaultSchemaGenerator; _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor); _jsonApiObjectNullabilityProcessor = new JsonApiObjectNullabilityProcessor(_schemaRepositoryAccessor); - - _resourceObjectSchemaGenerator = - new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceContextProvider, jsonApiOptions, _schemaRepositoryAccessor); + _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor); } public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository, MemberInfo memberInfo = null, ParameterInfo parameterInfo = null) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index e4c3791e20..baf380972d 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using Microsoft.OpenApi.Models; -using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.SwaggerGen; namespace JsonApiDotNetCore.OpenApi.SwaggerComponents @@ -11,38 +11,38 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents internal sealed class ResourceObjectSchemaGenerator { private readonly SchemaGenerator _defaultSchemaGenerator; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; private readonly bool _allowClientGeneratedIds; private readonly Func _createFieldObjectBuilderFactory; - public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceContextProvider resourceContextProvider, - IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor) + public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options, + ISchemaRepositoryAccessor schemaRepositoryAccessor) { ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); _defaultSchemaGenerator = defaultSchemaGenerator; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _schemaRepositoryAccessor = schemaRepositoryAccessor; - _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceContextProvider); - _allowClientGeneratedIds = jsonApiOptions.AllowClientGeneratedIds; + _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceGraph); + _allowClientGeneratedIds = options.AllowClientGeneratedIds; - _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceContextProvider, jsonApiOptions, - schemaRepositoryAccessor, _resourceTypeSchemaGenerator); + _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceGraph, options, schemaRepositoryAccessor, + _resourceTypeSchemaGenerator); } private static Func CreateFieldObjectBuilderFactory(SchemaGenerator defaultSchemaGenerator, - IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor, + IResourceGraph resourceGraph, IJsonApiOptions options, ISchemaRepositoryAccessor schemaRepositoryAccessor, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) { - NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; - ResourceNameFormatter resourceNameFormatter = new(namingStrategy); - var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceContextProvider); + JsonNamingPolicy namingPolicy = options.SerializerOptions.PropertyNamingPolicy; + ResourceNameFormatter resourceNameFormatter = new(namingPolicy); + var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceGraph); return resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, defaultSchemaGenerator, jsonApiSchemaIdSelector, resourceTypeSchemaGenerator); @@ -54,7 +54,7 @@ public OpenApiSchema GenerateSchema(Type resourceObjectType) (OpenApiSchema fullSchemaForResourceObject, OpenApiSchema referenceSchemaForResourceObject) = EnsureSchemasExist(resourceObjectType); - var resourceTypeInfo = ResourceTypeInfo.Create(resourceObjectType, _resourceContextProvider); + var resourceTypeInfo = ResourceTypeInfo.Create(resourceObjectType, _resourceGraph); ResourceFieldObjectSchemaBuilder fieldObjectBuilder = _createFieldObjectBuilderFactory(resourceTypeInfo); RemoveResourceIdIfPostResourceObject(resourceTypeInfo.ResourceObjectOpenType, fullSchemaForResourceObject); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs index 5dc13b24b8..67d98dbb85 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs @@ -22,14 +22,14 @@ private ResourceTypeInfo(Type resourceObjectType, Type resourceObjectOpenType, T ResourceType = resourceType; } - public static ResourceTypeInfo Create(Type resourceObjectType, IResourceContextProvider resourceContextProvider) + public static ResourceTypeInfo Create(Type resourceObjectType, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(resourceObjectType, nameof(resourceObjectType)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); Type resourceObjectOpenType = resourceObjectType.GetGenericTypeDefinition(); Type resourceType = resourceObjectType.GenericTypeArguments[0]; - ResourceContext resourceContext = resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = resourceGraph.GetResourceContext(resourceType); return new ResourceTypeInfo(resourceObjectType, resourceObjectOpenType, resourceType, resourceContext); } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs index 2ceba85e18..2c1b43916c 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs @@ -9,16 +9,16 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents internal sealed class ResourceTypeSchemaGenerator { private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly Dictionary _resourceTypeSchemaCache = new(); - public ResourceTypeSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, IResourceContextProvider resourceContextProvider) + public ResourceTypeSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _schemaRepositoryAccessor = schemaRepositoryAccessor; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public OpenApiSchema Get(Type resourceType) @@ -30,7 +30,7 @@ public OpenApiSchema Get(Type resourceType) return referenceSchema; } - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); var fullSchema = new OpenApiSchema { diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs index 453267d828..744a03a9e8 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs @@ -32,7 +32,7 @@ private void AssertIsNotDeclared(string localId) { if (_idsTracked.ContainsKey(localId)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Another local ID with the same name is already defined at this point.", Detail = $"Another local ID with name '{localId}' is already defined at this point." @@ -75,7 +75,7 @@ public string GetValue(string localId, string resourceType) if (item.ServerId == null) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Local ID cannot be both defined and used within the same operation.", Detail = $"Local ID '{localId}' cannot be both defined and used within the same operation." @@ -89,7 +89,7 @@ private void AssertIsDeclared(string localId) { if (!_idsTracked.ContainsKey(localId)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Server-generated value for local ID is not available at this point.", Detail = $"Server-generated value for local ID '{localId}' is not available at this point." @@ -101,7 +101,7 @@ private static void AssertSameResourceType(string currentType, string declaredTy { if (declaredType != currentType) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Type mismatch in local ID usage.", Detail = $"Local ID '{localId}' belongs to resource type '{declaredType}' instead of '{currentType}'." diff --git a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs index 6bd8e7a57e..d880ab7b42 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/LocalIdValidator.cs @@ -15,15 +15,15 @@ namespace JsonApiDotNetCore.AtomicOperations public sealed class LocalIdValidator { private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public LocalIdValidator(ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) + public LocalIdValidator(ILocalIdTracker localIdTracker, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _localIdTracker = localIdTracker; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public void Validate(IEnumerable operations) @@ -45,9 +45,10 @@ public void Validate(IEnumerable operations) } catch (JsonApiException exception) { - foreach (Error error in exception.Errors) + foreach (ErrorObject error in exception.Errors) { - error.Source.Pointer = $"/atomic:operations[{operationIndex}]" + error.Source.Pointer; + error.Source ??= new ErrorSource(); + error.Source.Pointer = $"/atomic:operations[{operationIndex}]{error.Source.Pointer}"; } throw; @@ -80,7 +81,7 @@ private void DeclareLocalId(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); _localIdTracker.Declare(resource.LocalId, resourceContext.PublicName); } } @@ -89,8 +90,7 @@ private void AssignLocalId(OperationContainer operation) { if (operation.Resource.LocalId != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); - + ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, "placeholder"); } } @@ -99,7 +99,7 @@ private void AssertLocalIdIsAssigned(IIdentifiable resource) { if (resource.LocalId != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resource.GetType()); _localIdTracker.GetValue(resource.LocalId, resourceContext.PublicName); } } diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs index b6d7bae21d..a71fa906cd 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationProcessorAccessor.cs @@ -14,15 +14,15 @@ namespace JsonApiDotNetCore.AtomicOperations [PublicAPI] public class OperationProcessorAccessor : IOperationProcessorAccessor { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IServiceProvider _serviceProvider; - public OperationProcessorAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) + public OperationProcessorAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _serviceProvider = serviceProvider; } @@ -38,7 +38,7 @@ public Task ProcessAsync(OperationContainer operation, Cance protected virtual IOperationProcessor ResolveProcessor(OperationContainer operation) { Type processorInterface = GetProcessorInterface(operation.Kind); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(operation.Resource.GetType()); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(operation.Resource.GetType()); Type processorType = processorInterface.MakeGenericType(resourceContext.ResourceType, resourceContext.IdentityType); return (IOperationProcessor)_serviceProvider.GetRequiredService(processorType); diff --git a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs index ffb2e19efc..8d531ea231 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/OperationsProcessor.cs @@ -19,28 +19,28 @@ public class OperationsProcessor : IOperationsProcessor private readonly IOperationProcessorAccessor _operationProcessorAccessor; private readonly IOperationsTransactionFactory _operationsTransactionFactory; private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; private readonly ITargetedFields _targetedFields; private readonly LocalIdValidator _localIdValidator; public OperationsProcessor(IOperationProcessorAccessor operationProcessorAccessor, IOperationsTransactionFactory operationsTransactionFactory, - ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider, IJsonApiRequest request, ITargetedFields targetedFields) + ILocalIdTracker localIdTracker, IResourceGraph resourceGraph, IJsonApiRequest request, ITargetedFields targetedFields) { ArgumentGuard.NotNull(operationProcessorAccessor, nameof(operationProcessorAccessor)); ArgumentGuard.NotNull(operationsTransactionFactory, nameof(operationsTransactionFactory)); ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); _operationProcessorAccessor = operationProcessorAccessor; _operationsTransactionFactory = operationsTransactionFactory; _localIdTracker = localIdTracker; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _request = request; _targetedFields = targetedFields; - _localIdValidator = new LocalIdValidator(_localIdTracker, _resourceContextProvider); + _localIdValidator = new LocalIdValidator(_localIdTracker, _resourceGraph); } /// @@ -77,9 +77,10 @@ public virtual async Task> ProcessAsync(IList> ProcessAsync(IList : ICreateProcessor { private readonly ICreateService _service; private readonly ILocalIdTracker _localIdTracker; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceContextProvider resourceContextProvider) + public CreateProcessor(ICreateService service, ILocalIdTracker localIdTracker, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(service, nameof(service)); ArgumentGuard.NotNull(localIdTracker, nameof(localIdTracker)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _service = service; _localIdTracker = localIdTracker; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } /// @@ -37,7 +37,7 @@ public virtual async Task ProcessAsync(OperationContainer op if (operation.Resource.LocalId != null) { string serverId = newResource != null ? newResource.StringId : operation.Resource.StringId; - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); _localIdTracker.Assign(operation.Resource.LocalId, resourceContext.PublicName, serverId); } diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index bee40d62c9..4b5d36c421 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,9 +1,8 @@ using System; using System.Data; +using System.Text.Json; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { @@ -12,15 +11,6 @@ namespace JsonApiDotNetCore.Configuration /// public interface IJsonApiOptions { - internal NamingStrategy SerializerNamingStrategy - { - get - { - var contractResolver = SerializerSettings.ContractResolver as DefaultContractResolver; - return contractResolver?.NamingStrategy ?? JsonApiOptions.DefaultNamingStrategy; - } - } - /// /// The URL prefix to use for exposed endpoints. /// @@ -40,7 +30,7 @@ internal NamingStrategy SerializerNamingStrategy bool IncludeJsonApiVersion { get; } /// - /// Whether or not stack traces should be serialized in objects. False by default. + /// Whether or not stack traces should be serialized in . False by default. /// bool IncludeExceptionStackTraceInErrors { get; } @@ -88,7 +78,7 @@ internal NamingStrategy SerializerNamingStrategy LinkTypes RelationshipLinks { get; } /// - /// Whether or not the total resource count should be included in all document-level meta objects. False by default. + /// Whether or not the total resource count should be included in top-level meta objects. This requires an additional database query. False by default. /// bool IncludeTotalResourceCount { get; } @@ -128,18 +118,6 @@ internal NamingStrategy SerializerNamingStrategy /// bool EnableLegacyFilterNotation { get; } - /// - /// Determines whether the serialization setting can be controlled using a query string - /// parameter. False by default. - /// - bool AllowQueryStringOverrideForSerializerNullValueHandling { get; } - - /// - /// Determines whether the serialization setting can be controlled using a query string - /// parameter. False by default. - /// - bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; } - /// /// Controls how many levels deep includes are allowed to be nested. For example, MaximumIncludeDepth=1 would allow ?include=articles but not /// ?include=articles.revisions. null by default, which means unconstrained. @@ -158,18 +136,25 @@ internal NamingStrategy SerializerNamingStrategy IsolationLevel? TransactionIsolationLevel { get; } /// - /// Specifies the settings that are used by the . Note that at some places a few settings are ignored, to ensure JSON:API - /// spec compliance. + /// Enables to customize the settings that are used by the . + /// /// - /// The next example changes the naming convention to kebab casing. + /// The next example sets the naming convention to camel casing. /// /// + JsonSerializerOptions SerializerOptions { get; } + + /// + /// Gets the settings used for deserializing request bodies. This value is based on and is intended for internal use. + /// + JsonSerializerOptions SerializerReadOptions { get; } + + /// + /// Gets the settings used for serializing response bodies. This value is based on and is intended for internal use. /// - JsonSerializerSettings SerializerSettings { get; } + JsonSerializerOptions SerializerWriteOptions { get; } } } diff --git a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs b/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs deleted file mode 100644 index 62f4e729a6..0000000000 --- a/src/JsonApiDotNetCore/Configuration/IResourceContextProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Configuration -{ - /// - /// Responsible for getting s from the . - /// - public interface IResourceContextProvider - { - /// - /// Gets the metadata for all registered resources. - /// - IReadOnlySet GetResourceContexts(); - - /// - /// Gets the resource metadata for the resource that is publicly exposed by the specified name. - /// - ResourceContext GetResourceContext(string publicName); - - /// - /// Gets the resource metadata for the specified resource type. - /// - ResourceContext GetResourceContext(Type resourceType); - - /// - /// Gets the resource metadata for the specified resource type. - /// - ResourceContext GetResourceContext() - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs index 3629b1b69a..b7216f0f8c 100644 --- a/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/IResourceGraph.cs @@ -8,14 +8,46 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Enables retrieving the exposed resource fields (attributes and relationships) of resources registered in the resource graph. + /// Metadata about the shape of JSON:API resources that your API serves and the relationships between them. The resource graph is built at application + /// startup and is exposed as a singleton through Dependency Injection. /// [PublicAPI] - public interface IResourceGraph : IResourceContextProvider + public interface IResourceGraph { /// - /// Gets all fields (attributes and relationships) for that are targeted by the selector. If no selector is provided, - /// all exposed fields are returned. + /// Gets the metadata for all registered resources. + /// + IReadOnlySet GetResourceContexts(); + + /// + /// Gets the resource metadata for the resource that is publicly exposed by the specified name. Throws an when + /// not found. + /// + ResourceContext GetResourceContext(string publicName); + + /// + /// Gets the resource metadata for the specified resource type. Throws an when not found. + /// + ResourceContext GetResourceContext(Type resourceType); + + /// + /// Gets the resource metadata for the specified resource type. Throws an when not found. + /// + ResourceContext GetResourceContext() + where TResource : class, IIdentifiable; + + /// + /// Attempts to get the resource metadata for the resource that is publicly exposed by the specified name. Returns null when not found. + /// + ResourceContext TryGetResourceContext(string publicName); + + /// + /// Attempts to get the resource metadata for the specified resource type. Returns null when not found. + /// + ResourceContext TryGetResourceContext(Type resourceType); + + /// + /// Gets the fields (attributes and relationships) for that are targeted by the selector. /// /// /// The resource type for which to retrieve fields. @@ -27,8 +59,7 @@ IReadOnlyCollection GetFields(Expression - /// Gets all attributes for that are targeted by the selector. If no selector is provided, all exposed attributes are - /// returned. + /// Gets the attributes for that are targeted by the selector. /// /// /// The resource type for which to retrieve attributes. @@ -40,8 +71,7 @@ IReadOnlyCollection GetAttributes(Expression - /// Gets all relationships for that are targeted by the selector. If no selector is provided, all exposed relationships - /// are returned. + /// Gets the relationships for that are targeted by the selector. /// /// /// The resource type for which to retrieve relationships. diff --git a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs index 27a70d314c..abe95d00bf 100644 --- a/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs +++ b/src/JsonApiDotNetCore/Configuration/InverseNavigationResolver.cs @@ -12,15 +12,15 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public sealed class InverseNavigationResolver : IInverseNavigationResolver { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IEnumerable _dbContextResolvers; - public InverseNavigationResolver(IResourceContextProvider resourceContextProvider, IEnumerable dbContextResolvers) + public InverseNavigationResolver(IResourceGraph resourceGraph, IEnumerable dbContextResolvers) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(dbContextResolvers, nameof(dbContextResolvers)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _dbContextResolvers = dbContextResolvers; } @@ -36,7 +36,7 @@ public void Resolve() private void Resolve(DbContext dbContext) { - foreach (ResourceContext resourceContext in _resourceContextProvider.GetResourceContexts().Where(context => context.Relationships.Any())) + foreach (ResourceContext resourceContext in _resourceGraph.GetResourceContexts().Where(context => context.Relationships.Any())) { IEntityType entityType = dbContext.Model.FindEntityType(resourceContext.ResourceType); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs index 6419e0327c..9200149b25 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs @@ -12,6 +12,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Building; +using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -87,6 +88,9 @@ public void AddResourceGraph(ICollection dbContextTypes, Action(); _services.AddScoped(); - _services.AddSingleton(sp => sp.GetRequiredService()); } private void AddRepositoryLayer() @@ -218,8 +221,6 @@ private void AddQueryStringLayer() _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(); RegisterDependentService(); @@ -227,8 +228,6 @@ private void AddQueryStringLayer() RegisterDependentService(); RegisterDependentService(); RegisterDependentService(); - RegisterDependentService(); - RegisterDependentService(); RegisterDependentService(); RegisterDependentService(); @@ -253,7 +252,6 @@ private void AddSerializationLayer() { _services.AddScoped(); _services.AddScoped(); - _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); _services.AddScoped(); diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index b898b9e205..4806c36248 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -1,8 +1,11 @@ +using System; using System.Data; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Threading; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using JsonApiDotNetCore.Serialization.JsonConverters; namespace JsonApiDotNetCore.Configuration { @@ -10,7 +13,14 @@ namespace JsonApiDotNetCore.Configuration [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - internal static readonly NamingStrategy DefaultNamingStrategy = new CamelCaseNamingStrategy(); + private Lazy _lazySerializerWriteOptions; + private Lazy _lazySerializerReadOptions; + + /// + JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; + + /// + JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value; // Workaround for https://github.com/dotnet/efcore/issues/21026 internal bool DisableTopPagination { get; set; } @@ -64,12 +74,6 @@ public sealed class JsonApiOptions : IJsonApiOptions /// public bool EnableLegacyFilterNotation { get; set; } - /// - public bool AllowQueryStringOverrideForSerializerNullValueHandling { get; set; } - - /// - public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; } - /// public int? MaximumIncludeDepth { get; set; } @@ -80,12 +84,37 @@ public sealed class JsonApiOptions : IJsonApiOptions public IsolationLevel? TransactionIsolationLevel { get; set; } /// - public JsonSerializerSettings SerializerSettings { get; } = new() + public JsonSerializerOptions SerializerOptions { get; } = new() { - ContractResolver = new DefaultContractResolver + // These are the options common to serialization and deserialization. + // At runtime, we actually use SerializerReadOptions and SerializerWriteOptions, which are customized copies of these settings, + // to overcome the limitation in System.Text.Json that the JsonPath is incorrect when using custom converters. + // Therefore we try to avoid using custom converters has much as possible. + // https://github.com/Tarmil/FSharp.SystemTextJson/issues/37 + // https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245 + + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Converters = { - NamingStrategy = DefaultNamingStrategy + new SingleOrManyDataConverterFactory() } }; + + public JsonApiOptions() + { + _lazySerializerReadOptions = + new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly); + + _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) + { + Converters = + { + new WriteOnlyDocumentConverter(), + new WriteOnlyRelationshipObjectConverter() + } + }, LazyThreadSafetyMode.PublicationOnly); + } } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs index f4d57f135c..1a661aaf1e 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiValidationFilter.cs @@ -52,7 +52,7 @@ public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEnt private static bool IsId(string key) { - return key == nameof(Identifiable.Id) || key.EndsWith("." + nameof(Identifiable.Id), StringComparison.Ordinal); + return key == nameof(Identifiable.Id) || key.EndsWith($".{nameof(Identifiable.Id)}", StringComparison.Ordinal); } private static bool IsAtPrimaryEndpoint(IJsonApiRequest request) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs index 5a18a8c2f1..f5b42e5499 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceContext.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceContext.cs @@ -7,12 +7,13 @@ namespace JsonApiDotNetCore.Configuration { /// - /// Provides metadata for a resource, such as its attributes and relationships. + /// Metadata about the shape of a JSON:API resource in the resource graph. /// [PublicAPI] public sealed class ResourceContext { - private IReadOnlyCollection _fields; + private readonly Dictionary _fieldsByPublicName = new(); + private readonly Dictionary _fieldsByPropertyName = new(); /// /// The publicly exposed resource name. @@ -29,6 +30,11 @@ public sealed class ResourceContext /// public Type IdentityType { get; } + /// + /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. + /// + public IReadOnlyCollection Fields { get; } + /// /// Exposed resource attributes. See https://jsonapi.org/format/#document-resource-object-attributes. /// @@ -44,11 +50,6 @@ public sealed class ResourceContext /// public IReadOnlyCollection EagerLoads { get; } - /// - /// Exposed resource attributes and relationships. See https://jsonapi.org/format/#document-resource-object-fields. - /// - public IReadOnlyCollection Fields => _fields ??= Attributes.Cast().Concat(Relationships).ToArray(); - /// /// Configures which links to show in the object for this resource type. Defaults to /// , which falls back to . @@ -92,12 +93,79 @@ public ResourceContext(string publicName, Type resourceType, Type identityType, PublicName = publicName; ResourceType = resourceType; IdentityType = identityType; + Fields = attributes.Cast().Concat(relationships).ToArray(); Attributes = attributes; Relationships = relationships; EagerLoads = eagerLoads; TopLevelLinks = topLevelLinks; ResourceLinks = resourceLinks; RelationshipLinks = relationshipLinks; + + foreach (ResourceFieldAttribute field in Fields) + { + _fieldsByPublicName.Add(field.PublicName, field); + _fieldsByPropertyName.Add(field.Property.Name, field); + } + } + + public AttrAttribute GetAttributeByPublicName(string publicName) + { + AttrAttribute attribute = TryGetAttributeByPublicName(publicName); + return attribute ?? throw new InvalidOperationException($"Attribute '{publicName}' does not exist on resource type '{PublicName}'."); + } + + public AttrAttribute TryGetAttributeByPublicName(string publicName) + { + ArgumentGuard.NotNull(publicName, nameof(publicName)); + + return _fieldsByPublicName.TryGetValue(publicName, out ResourceFieldAttribute field) && field is AttrAttribute attribute ? attribute : null; + } + + public AttrAttribute GetAttributeByPropertyName(string propertyName) + { + AttrAttribute attribute = TryGetAttributeByPropertyName(propertyName); + + return attribute ?? + throw new InvalidOperationException($"Attribute for property '{propertyName}' does not exist on resource type '{ResourceType.Name}'."); + } + + public AttrAttribute TryGetAttributeByPropertyName(string propertyName) + { + ArgumentGuard.NotNull(propertyName, nameof(propertyName)); + + return _fieldsByPropertyName.TryGetValue(propertyName, out ResourceFieldAttribute field) && field is AttrAttribute attribute ? attribute : null; + } + + public RelationshipAttribute GetRelationshipByPublicName(string publicName) + { + RelationshipAttribute relationship = TryGetRelationshipByPublicName(publicName); + return relationship ?? throw new InvalidOperationException($"Relationship '{publicName}' does not exist on resource type '{PublicName}'."); + } + + public RelationshipAttribute TryGetRelationshipByPublicName(string publicName) + { + ArgumentGuard.NotNull(publicName, nameof(publicName)); + + return _fieldsByPublicName.TryGetValue(publicName, out ResourceFieldAttribute field) && field is RelationshipAttribute relationship + ? relationship + : null; + } + + public RelationshipAttribute GetRelationshipByPropertyName(string propertyName) + { + RelationshipAttribute relationship = TryGetRelationshipByPropertyName(propertyName); + + return relationship ?? + throw new InvalidOperationException($"Relationship for property '{propertyName}' does not exist on resource type '{ResourceType.Name}'."); + } + + public RelationshipAttribute TryGetRelationshipByPropertyName(string propertyName) + { + ArgumentGuard.NotNull(propertyName, nameof(propertyName)); + + return _fieldsByPropertyName.TryGetValue(propertyName, out ResourceFieldAttribute field) && field is RelationshipAttribute relationship + ? relationship + : null; } public override string ToString() diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs index 26ada5f798..f65755b38d 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraph.cs @@ -14,37 +14,71 @@ namespace JsonApiDotNetCore.Configuration public sealed class ResourceGraph : IResourceGraph { private static readonly Type ProxyTargetAccessorType = Type.GetType("Castle.DynamicProxy.IProxyTargetAccessor, Castle.Core"); - private readonly IReadOnlySet _resourceContexts; + + private readonly IReadOnlySet _resourceContextSet; + private readonly Dictionary _resourceContextsByType = new(); + private readonly Dictionary _resourceContextsByPublicName = new(); public ResourceGraph(IReadOnlySet resourceContexts) { ArgumentGuard.NotNull(resourceContexts, nameof(resourceContexts)); - _resourceContexts = resourceContexts; + _resourceContextSet = resourceContexts; + + foreach (ResourceContext resourceContext in resourceContexts) + { + _resourceContextsByType.Add(resourceContext.ResourceType, resourceContext); + _resourceContextsByPublicName.Add(resourceContext.PublicName, resourceContext); + } } /// public IReadOnlySet GetResourceContexts() { - return _resourceContexts; + return _resourceContextSet; } /// public ResourceContext GetResourceContext(string publicName) + { + ResourceContext resourceContext = TryGetResourceContext(publicName); + + if (resourceContext == null) + { + throw new InvalidOperationException($"Resource type '{publicName}' does not exist."); + } + + return resourceContext; + } + + /// + public ResourceContext TryGetResourceContext(string publicName) { ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); - return _resourceContexts.SingleOrDefault(resourceContext => resourceContext.PublicName == publicName); + return _resourceContextsByPublicName.TryGetValue(publicName, out ResourceContext resourceContext) ? resourceContext : null; } /// public ResourceContext GetResourceContext(Type resourceType) + { + ResourceContext resourceContext = TryGetResourceContext(resourceType); + + if (resourceContext == null) + { + throw new InvalidOperationException($"Resource of type '{resourceType.Name}' does not exist."); + } + + return resourceContext; + } + + /// + public ResourceContext TryGetResourceContext(Type resourceType) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - return IsLazyLoadingProxyForResourceType(resourceType) - ? _resourceContexts.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType.BaseType) - : _resourceContexts.SingleOrDefault(resourceContext => resourceContext.ResourceType == resourceType); + Type typeToFind = IsLazyLoadingProxyForResourceType(resourceType) ? resourceType.BaseType : resourceType; + return _resourceContextsByType.TryGetValue(typeToFind!, out ResourceContext resourceContext) ? resourceContext : null; } private bool IsLazyLoadingProxyForResourceType(Type resourceType) diff --git a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs index 24bf42dc24..0ea1fb1a14 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceGraphBuilder.cs @@ -243,13 +243,15 @@ private Type TypeOrElementType(Type type) private string FormatResourceName(Type resourceType) { - var formatter = new ResourceNameFormatter(_options.SerializerNamingStrategy); + var formatter = new ResourceNameFormatter(_options.SerializerOptions.PropertyNamingPolicy); return formatter.FormatResourceName(resourceType); } private string FormatPropertyName(PropertyInfo resourceProperty) { - return _options.SerializerNamingStrategy.GetPropertyName(resourceProperty.Name, false); + return _options.SerializerOptions.PropertyNamingPolicy == null + ? resourceProperty.Name + : _options.SerializerOptions.PropertyNamingPolicy.ConvertName(resourceProperty.Name); } } } diff --git a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs index 70963bb1a3..93e5dda4ff 100644 --- a/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs +++ b/src/JsonApiDotNetCore/Configuration/ResourceNameFormatter.cs @@ -1,18 +1,18 @@ using System; using System.Reflection; +using System.Text.Json; using Humanizer; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Configuration { internal sealed class ResourceNameFormatter { - private readonly NamingStrategy _namingStrategy; + private readonly JsonNamingPolicy _namingPolicy; - public ResourceNameFormatter(NamingStrategy namingStrategy) + public ResourceNameFormatter(JsonNamingPolicy namingPolicy) { - _namingStrategy = namingStrategy; + _namingPolicy = namingPolicy; } /// @@ -20,9 +20,13 @@ public ResourceNameFormatter(NamingStrategy namingStrategy) /// public string FormatResourceName(Type resourceType) { - return resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute - ? attribute.PublicName - : _namingStrategy.GetPropertyName(resourceType.Name.Pluralize(), false); + if (resourceType.GetCustomAttribute(typeof(ResourceAttribute)) is ResourceAttribute attribute) + { + return attribute.PublicName; + } + + string publicName = resourceType.Name.Pluralize(); + return _namingPolicy != null ? _namingPolicy.ConvertName(publicName) : publicName; } } } diff --git a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs index b05fde176e..2c8c1fc5a2 100644 --- a/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Configuration/ServiceCollectionExtensions.cs @@ -6,8 +6,6 @@ using JsonApiDotNetCore.Errors; using JsonApiDotNetCore.Repositories; using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCore.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -57,25 +55,6 @@ private static void SetupApplicationBuilder(IServiceCollection services, Action< applicationBuilder.ConfigureServiceContainer(dbContextTypes); } - /// - /// Enables client serializers for sending requests and receiving responses in JSON:API format. Internally only used for testing. Will be extended in the - /// future to be part of a JsonApiClientDotNetCore package. - /// - public static IServiceCollection AddClientSerialization(this IServiceCollection services) - { - ArgumentGuard.NotNull(services, nameof(services)); - - services.AddScoped(); - - services.AddScoped(sp => - { - var graph = sp.GetRequiredService(); - return new RequestSerializer(graph, new ResourceObjectBuilder(graph, new ResourceObjectBuilderSettings())); - }); - - return services; - } - /// /// Adds IoC container registrations for the various JsonApiDotNetCore resource service interfaces, such as , /// and the various others. diff --git a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs index 5aa436bc22..7f6d266aa4 100644 --- a/src/JsonApiDotNetCore/Configuration/TypeLocator.cs +++ b/src/JsonApiDotNetCore/Configuration/TypeLocator.cs @@ -67,14 +67,14 @@ public ResourceDescriptor TryGetResourceDescriptor(Type type) if (!openGenericInterface.IsInterface || !openGenericInterface.IsGenericType || openGenericInterface != openGenericInterface.GetGenericTypeDefinition()) { - throw new ArgumentException($"Specified type '{openGenericInterface.FullName}' " + "is not an open generic interface.", + throw new ArgumentException($"Specified type '{openGenericInterface.FullName}' is not an open generic interface.", nameof(openGenericInterface)); } if (interfaceGenericTypeArguments.Length != openGenericInterface.GetGenericArguments().Length) { throw new ArgumentException( - $"Interface '{openGenericInterface.FullName}' " + $"requires {openGenericInterface.GetGenericArguments().Length} type parameters " + + $"Interface '{openGenericInterface.FullName}' requires {openGenericInterface.GetGenericArguments().Length} type parameters " + $"instead of {interfaceGenericTypeArguments.Length}.", nameof(interfaceGenericTypeArguments)); } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index c7bddff7b6..98a4c58afe 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -190,7 +190,7 @@ public virtual async Task PostAsync([FromBody] TResource resource if (_options.ValidateModelState && !ModelState.IsValid) { throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, - _options.SerializerNamingStrategy); + _options.SerializerOptions.PropertyNamingPolicy); } TResource newResource = await _create.CreateAsync(resource, cancellationToken); @@ -267,7 +267,7 @@ public virtual async Task PatchAsync(TId id, [FromBody] TResource if (_options.ValidateModelState && !ModelState.IsValid) { throw new InvalidModelStateException(ModelState, typeof(TResource), _options.IncludeExceptionStackTraceInErrors, - _options.SerializerNamingStrategy); + _options.SerializerOptions.PropertyNamingPolicy); } TResource updated = await _update.UpdateAsync(id, resource, cancellationToken); diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs index 64feb432ee..0ed536eb15 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs @@ -174,7 +174,7 @@ protected virtual void ValidateModelState(IEnumerable operat if (violations.Any()) { - throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerNamingStrategy); + throw new InvalidModelStateException(violations, _options.IncludeExceptionStackTraceInErrors, _options.SerializerOptions.PropertyNamingPolicy); } } diff --git a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs index 17db572550..88d9614cc7 100644 --- a/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/CoreJsonApiController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc; @@ -9,18 +10,21 @@ namespace JsonApiDotNetCore.Controllers /// public abstract class CoreJsonApiController : ControllerBase { - protected IActionResult Error(Error error) + protected IActionResult Error(ErrorObject error) { ArgumentGuard.NotNull(error, nameof(error)); return Error(error.AsEnumerable()); } - protected IActionResult Error(IEnumerable errors) + protected IActionResult Error(IEnumerable errors) { ArgumentGuard.NotNull(errors, nameof(errors)); - var document = new ErrorDocument(errors); + var document = new Document + { + Errors = errors.ToList() + }; return new ObjectResult(document) { diff --git a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs index f528ed6c96..782cf1f2ea 100644 --- a/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs +++ b/src/JsonApiDotNetCore/Errors/CannotClearRequiredRelationshipException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class CannotClearRequiredRelationshipException : JsonApiException { public CannotClearRequiredRelationshipException(string relationshipName, string resourceId, string resourceType) - : base(new Error(HttpStatusCode.BadRequest) + : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Failed to clear a required relationship.", Detail = $"The relationship '{relationshipName}' of resource type '{resourceType}' " + diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index 7f4f3ee004..4ca8586b17 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -4,12 +4,12 @@ using System.Linq; using System.Net; using System.Reflection; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.Errors { @@ -20,13 +20,13 @@ namespace JsonApiDotNetCore.Errors public sealed class InvalidModelStateException : JsonApiException { public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType, bool includeExceptionStackTraceInErrors, - NamingStrategy namingStrategy) - : this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingStrategy) + JsonNamingPolicy namingPolicy) + : this(FromModelStateDictionary(modelState, resourceType), includeExceptionStackTraceInErrors, namingPolicy) { } - public InvalidModelStateException(IEnumerable violations, bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy) - : base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingStrategy)) + public InvalidModelStateException(IEnumerable violations, bool includeExceptionStackTraceInErrors, JsonNamingPolicy namingPolicy) + : base(FromModelStateViolations(violations, includeExceptionStackTraceInErrors, namingPolicy)) { } @@ -54,50 +54,55 @@ private static void AddValidationErrors(ModelStateEntry entry, string propertyNa } } - private static IEnumerable FromModelStateViolations(IEnumerable violations, bool includeExceptionStackTraceInErrors, - NamingStrategy namingStrategy) + private static IEnumerable FromModelStateViolations(IEnumerable violations, bool includeExceptionStackTraceInErrors, + JsonNamingPolicy namingPolicy) { ArgumentGuard.NotNull(violations, nameof(violations)); - ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); - return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingStrategy)); + return violations.SelectMany(violation => FromModelStateViolation(violation, includeExceptionStackTraceInErrors, namingPolicy)); } - private static IEnumerable FromModelStateViolation(ModelStateViolation violation, bool includeExceptionStackTraceInErrors, - NamingStrategy namingStrategy) + private static IEnumerable FromModelStateViolation(ModelStateViolation violation, bool includeExceptionStackTraceInErrors, + JsonNamingPolicy namingPolicy) { if (violation.Error.Exception is JsonApiException jsonApiException) { - foreach (Error error in jsonApiException.Errors) + foreach (ErrorObject error in jsonApiException.Errors) { yield return error; } } else { - string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingStrategy); - string attributePath = violation.Prefix + attributeName; + string attributeName = GetDisplayNameForProperty(violation.PropertyName, violation.ResourceType, namingPolicy); + string attributePath = $"{violation.Prefix}{attributeName}"; yield return FromModelError(violation.Error, attributePath, includeExceptionStackTraceInErrors); } } - private static string GetDisplayNameForProperty(string propertyName, Type resourceType, NamingStrategy namingStrategy) + private static string GetDisplayNameForProperty(string propertyName, Type resourceType, JsonNamingPolicy namingPolicy) { PropertyInfo property = resourceType.GetProperty(propertyName); if (property != null) { var attrAttribute = property.GetCustomAttribute(); - return attrAttribute?.PublicName ?? namingStrategy.GetPropertyName(property.Name, false); + + if (attrAttribute?.PublicName != null) + { + return attrAttribute.PublicName; + } + + return namingPolicy != null ? namingPolicy.ConvertName(property.Name) : property.Name; } return propertyName; } - private static Error FromModelError(ModelError modelError, string attributePath, bool includeExceptionStackTraceInErrors) + private static ErrorObject FromModelError(ModelError modelError, string attributePath, bool includeExceptionStackTraceInErrors) { - var error = new Error(HttpStatusCode.UnprocessableEntity) + var error = new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Input validation failed.", Detail = modelError.ErrorMessage, @@ -111,7 +116,10 @@ private static Error FromModelError(ModelError modelError, string attributePath, if (includeExceptionStackTraceInErrors && modelError.Exception != null) { - error.Meta.IncludeExceptionStackTrace(modelError.Exception.Demystify()); + string[] stackTraceLines = modelError.Exception.Demystify().ToString().Split(Environment.NewLine); + + error.Meta ??= new Dictionary(); + error.Meta["StackTrace"] = stackTraceLines; } return error; diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs index 5e704b8e3c..fb02f2a5e4 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryException.cs @@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Errors public sealed class InvalidQueryException : JsonApiException { public InvalidQueryException(string reason, Exception exception) - : base(new Error(HttpStatusCode.BadRequest) + : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = reason, Detail = exception?.Message diff --git a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs index 99a4eb381e..949710d62a 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidQueryStringParameterException.cs @@ -14,11 +14,11 @@ public sealed class InvalidQueryStringParameterException : JsonApiException public string QueryParameterName { get; } public InvalidQueryStringParameterException(string queryParameterName, string genericMessage, string specificMessage, Exception innerException = null) - : base(new Error(HttpStatusCode.BadRequest) + : base(new ErrorObject(HttpStatusCode.BadRequest) { Title = genericMessage, Detail = specificMessage, - Source = + Source = new ErrorSource { Parameter = queryParameterName } diff --git a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs index fe860ac0fd..c435c66b2d 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidRequestBodyException.cs @@ -13,9 +13,9 @@ namespace JsonApiDotNetCore.Errors public sealed class InvalidRequestBodyException : JsonApiException { public InvalidRequestBodyException(string reason, string details, string requestBody, Exception innerException = null) - : base(new Error(HttpStatusCode.UnprocessableEntity) + : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { - Title = reason != null ? "Failed to deserialize request body: " + reason : "Failed to deserialize request body.", + Title = reason != null ? $"Failed to deserialize request body: {reason}" : "Failed to deserialize request body.", Detail = FormatErrorDetail(details, requestBody, innerException) }, innerException) { diff --git a/src/JsonApiDotNetCore/Errors/JsonApiException.cs b/src/JsonApiDotNetCore/Errors/JsonApiException.cs index 6ad1a2021c..13d3b6a745 100644 --- a/src/JsonApiDotNetCore/Errors/JsonApiException.cs +++ b/src/JsonApiDotNetCore/Errors/JsonApiException.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Errors { @@ -13,17 +15,18 @@ namespace JsonApiDotNetCore.Errors [PublicAPI] public class JsonApiException : Exception { - private static readonly JsonSerializerSettings ErrorSerializerSettings = new() + private static readonly JsonSerializerOptions SerializerOptions = new() { - NullValueHandling = NullValueHandling.Ignore, - Formatting = Formatting.Indented + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - public IReadOnlyList Errors { get; } + public IReadOnlyList Errors { get; } - public override string Message => "Errors = " + JsonConvert.SerializeObject(Errors, ErrorSerializerSettings); + public override string Message => $"Errors = {JsonSerializer.Serialize(Errors, SerializerOptions)}"; - public JsonApiException(Error error, Exception innerException = null) + public JsonApiException(ErrorObject error, Exception innerException = null) : base(null, innerException) { ArgumentGuard.NotNull(error, nameof(error)); @@ -31,10 +34,10 @@ public JsonApiException(Error error, Exception innerException = null) Errors = error.AsArray(); } - public JsonApiException(IEnumerable errors, Exception innerException = null) + public JsonApiException(IEnumerable errors, Exception innerException = null) : base(null, innerException) { - List errorList = errors?.ToList(); + List errorList = errors?.ToList(); ArgumentGuard.NotNullNorEmpty(errorList, nameof(errors)); Errors = errorList; diff --git a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs index e092150ba5..81e13baeda 100644 --- a/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs +++ b/src/JsonApiDotNetCore/Errors/MissingTransactionSupportException.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCore.Errors public sealed class MissingTransactionSupportException : JsonApiException { public MissingTransactionSupportException(string resourceType) - : base(new Error(HttpStatusCode.UnprocessableEntity) + : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported resource type in atomic:operations request.", - Detail = $"Operations on resources of type '{resourceType}' " + "cannot be used because transaction support is unavailable." + Detail = $"Operations on resources of type '{resourceType}' cannot be used because transaction support is unavailable." }) { } diff --git a/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs b/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs index 9da29c521b..909f4a047d 100644 --- a/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs +++ b/src/JsonApiDotNetCore/Errors/NonParticipatingTransactionException.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCore.Errors public sealed class NonParticipatingTransactionException : JsonApiException { public NonParticipatingTransactionException() - : base(new Error(HttpStatusCode.UnprocessableEntity) + : base(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Unsupported combination of resource types in atomic:operations request.", - Detail = "All operations need to participate in a single shared transaction, " + "which is not the case for this request." + Detail = "All operations need to participate in a single shared transaction, which is not the case for this request." }) { } diff --git a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs index 864a0c7606..07b5d0e25a 100644 --- a/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/RelationshipNotFoundException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class RelationshipNotFoundException : JsonApiException { public RelationshipNotFoundException(string relationshipName, string resourceType) - : base(new Error(HttpStatusCode.NotFound) + : base(new ErrorObject(HttpStatusCode.NotFound) { Title = "The requested relationship does not exist.", Detail = $"Resource of type '{resourceType}' does not contain a relationship named '{relationshipName}'." diff --git a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs index 3d3c5b163b..a4edbc0d5f 100644 --- a/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/RequestMethodNotAllowedException.cs @@ -14,7 +14,7 @@ public sealed class RequestMethodNotAllowedException : JsonApiException public HttpMethod Method { get; } public RequestMethodNotAllowedException(HttpMethod method) - : base(new Error(HttpStatusCode.MethodNotAllowed) + : base(new ErrorObject(HttpStatusCode.MethodNotAllowed) { Title = "The request method is not allowed.", Detail = $"Endpoint does not support {method} requests." diff --git a/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs b/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs index 34bc21dff5..384289576d 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceAlreadyExistsException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceAlreadyExistsException : JsonApiException { public ResourceAlreadyExistsException(string resourceId, string resourceType) - : base(new Error(HttpStatusCode.Conflict) + : base(new ErrorObject(HttpStatusCode.Conflict) { Title = "Another resource with the specified ID already exists.", Detail = $"Another resource of type '{resourceType}' with ID '{resourceId}' already exists." diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs index 4266f987b3..11a96cc436 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdInCreateResourceNotAllowedException.cs @@ -11,12 +11,12 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceIdInCreateResourceNotAllowedException : JsonApiException { public ResourceIdInCreateResourceNotAllowedException(int? atomicOperationIndex = null) - : base(new Error(HttpStatusCode.Forbidden) + : base(new ErrorObject(HttpStatusCode.Forbidden) { Title = atomicOperationIndex == null ? "Specifying the resource ID in POST requests is not allowed." : "Specifying the resource ID in operations that create a resource is not allowed.", - Source = + Source = new ErrorSource { Pointer = atomicOperationIndex != null ? $"/atomic:operations[{atomicOperationIndex}]/data/id" : "/data/id" } diff --git a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs index 721c17e7cd..fdfd6b6fe9 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceIdMismatchException.cs @@ -11,10 +11,10 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceIdMismatchException : JsonApiException { public ResourceIdMismatchException(string bodyId, string endpointId, string requestPath) - : base(new Error(HttpStatusCode.Conflict) + : base(new ErrorObject(HttpStatusCode.Conflict) { Title = "Resource ID mismatch between request body and endpoint URL.", - Detail = $"Expected resource ID '{endpointId}' in PATCH request body " + $"at endpoint '{requestPath}', instead of '{bodyId}'." + Detail = $"Expected resource ID '{endpointId}' in PATCH request body at endpoint '{requestPath}', instead of '{bodyId}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs index 4e63220269..50a05b70ff 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceNotFoundException.cs @@ -11,7 +11,7 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceNotFoundException : JsonApiException { public ResourceNotFoundException(string resourceId, string resourceType) - : base(new Error(HttpStatusCode.NotFound) + : base(new ErrorObject(HttpStatusCode.NotFound) { Title = "The requested resource does not exist.", Detail = $"Resource of type '{resourceType}' with ID '{resourceId}' does not exist." diff --git a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs index 83f28a14f9..9957694d0d 100644 --- a/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourceTypeMismatchException.cs @@ -13,11 +13,11 @@ namespace JsonApiDotNetCore.Errors public sealed class ResourceTypeMismatchException : JsonApiException { public ResourceTypeMismatchException(HttpMethod method, string requestPath, ResourceContext expected, ResourceContext actual) - : base(new Error(HttpStatusCode.Conflict) + : base(new ErrorObject(HttpStatusCode.Conflict) { Title = "Resource type mismatch between request body and endpoint URL.", - Detail = $"Expected resource of type '{expected.PublicName}' in {method} " + - $"request body at endpoint '{requestPath}', instead of '{actual?.PublicName}'." + Detail = $"Expected resource of type '{expected.PublicName}' in {method} request body at endpoint " + + $"'{requestPath}', instead of '{actual.PublicName}'." }) { } diff --git a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs index d749bca5fa..0cb1f9cecb 100644 --- a/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs +++ b/src/JsonApiDotNetCore/Errors/ResourcesInRelationshipsNotFoundException.cs @@ -17,7 +17,7 @@ public ResourcesInRelationshipsNotFoundException(IEnumerable - diff --git a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs index 72d635b5f3..d7d6349b68 100644 --- a/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs +++ b/src/JsonApiDotNetCore/Middleware/AsyncJsonApiExceptionFilter.cs @@ -26,11 +26,11 @@ public Task OnExceptionAsync(ExceptionContext context) if (context.HttpContext.IsJsonApiRequest()) { - ErrorDocument errorDocument = _exceptionHandler.HandleException(context.Exception); + Document document = _exceptionHandler.HandleException(context.Exception); - context.Result = new ObjectResult(errorDocument) + context.Result = new ObjectResult(document) { - StatusCode = (int)errorDocument.GetErrorStatusCode() + StatusCode = (int)document.GetErrorStatusCode() }; } diff --git a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs index 2006ad62de..5dc6bf6e5d 100644 --- a/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Net; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; @@ -26,7 +27,7 @@ public ExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options) _logger = loggerFactory.CreateLogger(); } - public ErrorDocument HandleException(Exception exception) + public Document HandleException(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); @@ -69,33 +70,42 @@ protected virtual string GetLogMessage(Exception exception) return exception.Message; } - protected virtual ErrorDocument CreateErrorDocument(Exception exception) + protected virtual Document CreateErrorDocument(Exception exception) { ArgumentGuard.NotNull(exception, nameof(exception)); - IReadOnlyList errors = exception is JsonApiException jsonApiException ? jsonApiException.Errors : - exception is OperationCanceledException ? new Error((HttpStatusCode)499) + IReadOnlyList errors = exception is JsonApiException jsonApiException ? jsonApiException.Errors : + exception is OperationCanceledException ? new ErrorObject((HttpStatusCode)499) { Title = "Request execution was canceled." - }.AsArray() : new Error(HttpStatusCode.InternalServerError) + }.AsArray() : new ErrorObject(HttpStatusCode.InternalServerError) { Title = "An unhandled error occurred while processing this request.", Detail = exception.Message }.AsArray(); - foreach (Error error in errors) + foreach (ErrorObject error in errors) { ApplyOptions(error, exception); } - return new ErrorDocument(errors); + return new Document + { + Errors = errors.ToList() + }; } - private void ApplyOptions(Error error, Exception exception) + private void ApplyOptions(ErrorObject error, Exception exception) { Exception resultException = exception is InvalidModelStateException ? null : exception; - error.Meta.IncludeExceptionStackTrace(_options.IncludeExceptionStackTraceInErrors ? resultException : null); + if (resultException != null && _options.IncludeExceptionStackTraceInErrors) + { + string[] stackTraceLines = resultException.ToString().Split(Environment.NewLine); + + error.Meta ??= new Dictionary(); + error.Meta["StackTrace"] = stackTraceLines; + } } } } diff --git a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs index 2521794c08..9f44e33a96 100644 --- a/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs +++ b/src/JsonApiDotNetCore/Middleware/IExceptionHandler.cs @@ -4,10 +4,10 @@ namespace JsonApiDotNetCore.Middleware { /// - /// Central place to handle all exceptions. Log them and translate into Error response. + /// Central place to handle all exceptions, such as log them and translate into error response. /// public interface IExceptionHandler { - ErrorDocument HandleException(Exception exception); + Document HandleException(Exception exception); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index efc3613979..30ea5d9108 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -1,14 +1,13 @@ using System; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -17,7 +16,6 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Middleware { @@ -41,41 +39,41 @@ public JsonApiMiddleware(RequestDelegate next, IHttpContextAccessor httpContextA } public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options, - IJsonApiRequest request, IResourceContextProvider resourceContextProvider, ILogger logger) + IJsonApiRequest request, IResourceGraph resourceGraph, ILogger logger) { ArgumentGuard.NotNull(httpContext, nameof(httpContext)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(logger, nameof(logger)); using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions)) { return; } RouteValueDictionary routeValues = httpContext.GetRouteData().Values; - ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider); + ResourceContext primaryResourceContext = TryCreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceGraph); if (primaryResourceContext != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) { return; } - SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request); + SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceGraph, httpContext.Request); httpContext.RegisterJsonApiRequest(); } else if (IsRouteForOperations(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions)) { return; } @@ -102,13 +100,17 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } } - private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings) + private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions) { if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { - await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.PreconditionFailed) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed) { - Title = "Detection of mid-air edit collisions using ETags is not supported." + Title = "Detection of mid-air edit collisions using ETags is not supported.", + Source = new ErrorSource + { + Header = "If-Match" + } }); return false; @@ -117,8 +119,8 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso return true; } - private static ResourceContext CreatePrimaryResourceContext(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, - IResourceContextProvider resourceContextProvider) + private static ResourceContext TryCreatePrimaryResourceContext(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, + IResourceGraph resourceGraph) { Endpoint endpoint = httpContext.GetEndpoint(); var controllerActionDescriptor = endpoint?.Metadata.GetMetadata(); @@ -130,7 +132,7 @@ private static ResourceContext CreatePrimaryResourceContext(HttpContext httpCont if (resourceType != null) { - return resourceContextProvider.GetResourceContext(resourceType); + return resourceGraph.GetResourceContext(resourceType); } } @@ -138,7 +140,7 @@ private static ResourceContext CreatePrimaryResourceContext(HttpContext httpCont } private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, - JsonSerializerSettings serializerSettings) + JsonSerializerOptions serializerOptions) { string contentType = httpContext.Request.ContentType; @@ -146,10 +148,14 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon // Justification: Workaround for https://github.com/dotnet/aspnetcore/issues/32097 (fixed in .NET 6) if (contentType != null && contentType != allowedContentType) { - await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.UnsupportedMediaType) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.UnsupportedMediaType) { Title = "The specified Content-Type header value is not supported.", - Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' " + "for the Content-Type header value." + Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' for the Content-Type header value.", + Source = new ErrorSource + { + Header = "Content-Type" + } }); return false; @@ -159,7 +165,7 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon } private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, - JsonSerializerSettings serializerSettings) + JsonSerializerOptions serializerOptions) { string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept"); @@ -192,10 +198,14 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a if (!seenCompatibleMediaType) { - await FlushResponseAsync(httpContext.Response, serializerSettings, new Error(HttpStatusCode.NotAcceptable) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.NotAcceptable) { Title = "The specified Accept header value does not contain any supported media types.", - Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values." + Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values.", + Source = new ErrorSource + { + Header = "Accept" + } }); return false; @@ -204,32 +214,22 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a return true; } - private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerSettings serializerSettings, Error error) + private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error) { httpResponse.ContentType = HeaderConstants.MediaType; httpResponse.StatusCode = (int)error.StatusCode; - var serializer = JsonSerializer.CreateDefault(serializerSettings); - serializer.ApplyErrorSettings(); - - // https://github.com/JamesNK/Newtonsoft.Json/issues/1193 - await using (var stream = new MemoryStream()) + var errorDocument = new Document { - 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(httpResponse.Body); - } + Errors = error.AsList() + }; + await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions); await httpResponse.Body.FlushAsync(); } private static void SetupResourceRequest(JsonApiRequest request, ResourceContext primaryResourceContext, RouteValueDictionary routeValues, - IResourceContextProvider resourceContextProvider, HttpRequest httpRequest) + IResourceGraph resourceGraph, HttpRequest httpRequest) { request.IsReadOnly = httpRequest.Method == HttpMethod.Get.Method || httpRequest.Method == HttpMethod.Head.Method; request.PrimaryResource = primaryResourceContext; @@ -252,13 +252,12 @@ private static void SetupResourceRequest(JsonApiRequest request, ResourceContext // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - RelationshipAttribute requestRelationship = - primaryResourceContext.Relationships.SingleOrDefault(relationship => relationship.PublicName == relationshipName); + RelationshipAttribute requestRelationship = primaryResourceContext.TryGetRelationshipByPublicName(relationshipName); if (requestRelationship != null) { request.Relationship = requestRelationship; - request.SecondaryResource = resourceContextProvider.GetResourceContext(requestRelationship.RightType); + request.SecondaryResource = resourceGraph.GetResourceContext(requestRelationship.RightType); } } else diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs index 51a831a5b6..af38f89dad 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs @@ -32,18 +32,18 @@ namespace JsonApiDotNetCore.Middleware public sealed class JsonApiRoutingConvention : IJsonApiRoutingConvention { private readonly IJsonApiOptions _options; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly Dictionary _registeredControllerNameByTemplate = new(); private readonly Dictionary _resourceContextPerControllerTypeMap = new(); private readonly Dictionary _controllerPerResourceContextMap = new(); - public JsonApiRoutingConvention(IJsonApiOptions options, IResourceContextProvider resourceContextProvider) + public JsonApiRoutingConvention(IJsonApiOptions options, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _options = options; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } /// @@ -64,7 +64,7 @@ public string GetControllerNameForResourceType(Type resourceType) { ArgumentGuard.NotNull(resourceType, nameof(resourceType)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (_controllerPerResourceContextMap.TryGetValue(resourceContext, out ControllerModel controllerModel)) @@ -90,7 +90,7 @@ public void Apply(ApplicationModel application) if (resourceType != null) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(resourceType); if (resourceContext != null) { @@ -146,7 +146,10 @@ private string TemplateFromResource(ControllerModel model) /// private string TemplateFromController(ControllerModel model) { - string controllerName = _options.SerializerNamingStrategy.GetPropertyName(model.ControllerName, false); + string controllerName = _options.SerializerOptions.PropertyNamingPolicy == null + ? model.ControllerName + : _options.SerializerOptions.PropertyNamingPolicy.ConvertName(model.ControllerName); + return $"{_options.Namespace}/{controllerName}"; } diff --git a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs index bd9d4f12ac..2ed2dbab48 100644 --- a/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs +++ b/src/JsonApiDotNetCore/Middleware/TraceLogWriter.cs @@ -2,12 +2,24 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Middleware { - internal sealed class TraceLogWriter + internal abstract class TraceLogWriter + { + protected static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + ReferenceHandler = ReferenceHandler.Preserve + }; + } + + internal sealed class TraceLogWriter : TraceLogWriter { private readonly ILogger _logger; @@ -126,12 +138,9 @@ private static string SerializeObject(object value) { try { - // It turns out setting ReferenceLoopHandling to something other than Error only takes longer to fail. - // This is because Newtonsoft.Json always tries to serialize the first element in a graph. And with - // EF Core models, that one is often recursive, resulting in either StackOverflowException or OutOfMemoryException. - return JsonConvert.SerializeObject(value, Formatting.Indented); + return JsonSerializer.Serialize(value, SerializerOptions); } - catch (JsonSerializationException) + catch (JsonException) { // Never crash as a result of logging, this is best-effort only. return "object"; diff --git a/src/JsonApiDotNetCore/Properties/launchSettings.json b/src/JsonApiDotNetCore/Properties/launchSettings.json deleted file mode 100644 index 233fb4a18e..0000000000 --- a/src/JsonApiDotNetCore/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:63521/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "JsonApiDotNetCore": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:63522/" - } - } -} diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs index 5461fb994c..d375f72a16 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/FilterParser.cs @@ -13,19 +13,19 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class FilterParser : QueryExpressionParser { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IResourceFactory _resourceFactory; private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public FilterParser(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, + public FilterParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory, Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + : base(resourceGraph) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _resourceFactory = resourceFactory; _validateSingleFieldCallback = validateSingleFieldCallback; } @@ -268,7 +268,7 @@ private FilterExpression ParseFilterInHas(HasManyAttribute hasManyRelationship) ResourceContext outerScopeBackup = _resourceContextInScope; Type innerResourceType = hasManyRelationship.RightType; - _resourceContextInScope = _resourceContextProvider.GetResourceContext(innerResourceType); + _resourceContextInScope = _resourceGraph.GetResourceContext(innerResourceType); FilterExpression filter = ParseFilter(); diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index b6cb69d6b9..9d6f394d75 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -17,9 +17,8 @@ public class IncludeParser : QueryExpressionParser private readonly Action _validateSingleRelationshipCallback; private ResourceContext _resourceContextInScope; - public IncludeParser(IResourceContextProvider resourceContextProvider, - Action validateSingleRelationshipCallback = null) - : base(resourceContextProvider) + public IncludeParser(IResourceGraph resourceGraph, Action validateSingleRelationshipCallback = null) + : base(resourceGraph) { _validateSingleRelationshipCallback = validateSingleRelationshipCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs index 54c56b2f13..62f8dd6a91 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/PaginationParser.cs @@ -14,9 +14,8 @@ public class PaginationParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public PaginationParser(IResourceContextProvider resourceContextProvider, - Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + public PaginationParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) + : base(resourceGraph) { _validateSingleFieldCallback = validateSingleFieldCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs index 99a195bb6c..65cf321347 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryExpressionParser.cs @@ -21,9 +21,9 @@ public abstract class QueryExpressionParser protected Stack TokenStack { get; private set; } private protected ResourceFieldChainResolver ChainResolver { get; } - protected QueryExpressionParser(IResourceContextProvider resourceContextProvider) + protected QueryExpressionParser(IResourceGraph resourceGraph) { - ChainResolver = new ResourceFieldChainResolver(resourceContextProvider); + ChainResolver = new ResourceFieldChainResolver(resourceGraph); } /// @@ -74,7 +74,7 @@ protected void EatText(string text) { if (!TokenStack.TryPop(out Token token) || token.Kind != TokenKind.Text || token.Value != text) { - throw new QueryParseException(text + " expected."); + throw new QueryParseException($"{text} expected."); } } @@ -83,7 +83,7 @@ protected void EatSingleCharacterToken(TokenKind kind) if (!TokenStack.TryPop(out Token token) || token.Kind != kind) { char ch = QueryTokenizer.SingleCharacterToTokenKinds.Single(pair => pair.Value == kind).Key; - throw new QueryParseException(ch + " expected."); + throw new QueryParseException($"{ch} expected."); } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs index feabc655ea..7d98110362 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/QueryStringParameterScopeParser.cs @@ -14,9 +14,9 @@ public class QueryStringParameterScopeParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public QueryStringParameterScopeParser(IResourceContextProvider resourceContextProvider, FieldChainRequirements chainRequirements, + public QueryStringParameterScopeParser(IResourceGraph resourceGraph, FieldChainRequirements chainRequirements, Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + : base(resourceGraph) { _chainRequirements = chainRequirements; _validateSingleFieldCallback = validateSingleFieldCallback; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs index 71c86ed9ba..edb7a774f2 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/ResourceFieldChainResolver.cs @@ -11,13 +11,13 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing /// internal sealed class ResourceFieldChainResolver { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public ResourceFieldChainResolver(IResourceContextProvider resourceContextProvider) + public ResourceFieldChainResolver(IResourceGraph resourceGraph) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } /// @@ -38,7 +38,7 @@ public IImmutableList ResolveToManyChain(ResourceContext validateCallback?.Invoke(relationship, nextResourceContext, path); chainBuilder.Add(relationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); } string lastName = publicNameParts[^1]; @@ -75,7 +75,7 @@ public IImmutableList ResolveRelationshipChain(ResourceC validateCallback?.Invoke(relationship, nextResourceContext, path); chainBuilder.Add(relationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); } return chainBuilder.ToImmutable(); @@ -103,7 +103,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; @@ -139,7 +139,7 @@ public IImmutableList ResolveToOneChainEndingInToMany(Re validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; @@ -176,7 +176,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute validateCallback?.Invoke(toOneRelationship, nextResourceContext, path); chainBuilder.Add(toOneRelationship); - nextResourceContext = _resourceContextProvider.GetResourceContext(toOneRelationship.RightType); + nextResourceContext = _resourceGraph.GetResourceContext(toOneRelationship.RightType); } string lastName = publicNameParts[^1]; @@ -197,7 +197,7 @@ public IImmutableList ResolveToOneChainEndingInAttribute private RelationshipAttribute GetRelationship(string publicName, ResourceContext resourceContext, string path) { - RelationshipAttribute relationship = resourceContext.Relationships.FirstOrDefault(nextRelationship => nextRelationship.PublicName == publicName); + RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(publicName); if (relationship == null) { @@ -239,7 +239,7 @@ private RelationshipAttribute GetToOneRelationship(string publicName, ResourceCo private AttrAttribute GetAttribute(string publicName, ResourceContext resourceContext, string path) { - AttrAttribute attribute = resourceContext.Attributes.FirstOrDefault(nextAttribute => nextAttribute.PublicName == publicName); + AttrAttribute attribute = resourceContext.TryGetAttributeByPublicName(publicName); if (attribute == null) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs index d9f0413935..4d588bacbb 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SortParser.cs @@ -14,9 +14,8 @@ public class SortParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContextInScope; - public SortParser(IResourceContextProvider resourceContextProvider, - Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + public SortParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) + : base(resourceGraph) { _validateSingleFieldCallback = validateSingleFieldCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs index dd45d1fe24..ea44dca7e5 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldSetParser.cs @@ -14,9 +14,8 @@ public class SparseFieldSetParser : QueryExpressionParser private readonly Action _validateSingleFieldCallback; private ResourceContext _resourceContext; - public SparseFieldSetParser(IResourceContextProvider resourceContextProvider, - Action validateSingleFieldCallback = null) - : base(resourceContextProvider) + public SparseFieldSetParser(IResourceGraph resourceGraph, Action validateSingleFieldCallback = null) + : base(resourceGraph) { _validateSingleFieldCallback = validateSingleFieldCallback; } diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs index be9764f1ff..fec5356282 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/SparseFieldTypeParser.cs @@ -9,12 +9,12 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing [PublicAPI] public class SparseFieldTypeParser : QueryExpressionParser { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public SparseFieldTypeParser(IResourceContextProvider resourceContextProvider) - : base(resourceContextProvider) + public SparseFieldTypeParser(IResourceGraph resourceGraph) + : base(resourceGraph) { - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public ResourceContext Parse(string source) @@ -56,7 +56,7 @@ private ResourceContext ParseResourceName() private ResourceContext GetResourceContext(string publicName) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(publicName); + ResourceContext resourceContext = _resourceGraph.TryGetResourceContext(publicName); if (resourceContext == null) { diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs index 236a3d80f6..6bc933cfc0 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs @@ -17,7 +17,7 @@ public class QueryLayerComposer : IQueryLayerComposer { private readonly CollectionConverter _collectionConverter = new(); private readonly IEnumerable _constraintProviders; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IJsonApiOptions _options; private readonly IPaginationContext _paginationContext; @@ -25,12 +25,12 @@ public class QueryLayerComposer : IQueryLayerComposer private readonly IEvaluatedIncludeCache _evaluatedIncludeCache; private readonly SparseFieldSetCache _sparseFieldSetCache; - public QueryLayerComposer(IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, + public QueryLayerComposer(IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiOptions options, IPaginationContext paginationContext, ITargetedFields targetedFields, IEvaluatedIncludeCache evaluatedIncludeCache) { ArgumentGuard.NotNull(constraintProviders, nameof(constraintProviders)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); @@ -38,7 +38,7 @@ public QueryLayerComposer(IEnumerable constraintProvid ArgumentGuard.NotNull(evaluatedIncludeCache, nameof(evaluatedIncludeCache)); _constraintProviders = constraintProviders; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _resourceDefinitionAccessor = resourceDefinitionAccessor; _options = options; _paginationContext = paginationContext; @@ -169,7 +169,7 @@ private IImmutableList ProcessIncludeSet(IImmutableLis // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(includeElement.Relationship.RightType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(includeElement.Relationship.RightType); bool isToManyRelationship = includeElement.Relationship is HasManyAttribute; var child = new QueryLayer(resourceContext) @@ -367,7 +367,7 @@ public QueryLayer ComposeForGetRelationshipRightIds(RelationshipAttribute relati ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - ResourceContext rightResourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(relationship.RightType); AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); object[] typedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); @@ -392,10 +392,10 @@ public QueryLayer ComposeForHasMany(HasManyAttribute hasManyRelationship, T ArgumentGuard.NotNull(hasManyRelationship, nameof(hasManyRelationship)); ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds)); - ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.LeftType); + ResourceContext leftResourceContext = _resourceGraph.GetResourceContext(hasManyRelationship.LeftType); AttrAttribute leftIdAttribute = GetIdAttribute(leftResourceContext); - ResourceContext rightResourceContext = _resourceContextProvider.GetResourceContext(hasManyRelationship.RightType); + ResourceContext rightResourceContext = _resourceGraph.GetResourceContext(hasManyRelationship.RightType); AttrAttribute rightIdAttribute = GetIdAttribute(rightResourceContext); object[] rightTypedIds = rightResourceIds.Select(resource => resource.GetTypedId()).ToArray(); @@ -493,7 +493,7 @@ protected virtual IDictionary GetProjectionF private static AttrAttribute GetIdAttribute(ResourceContext resourceContext) { - return resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); + return resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs index 7f90e334ad..081cf0be34 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/IncludeClauseBuilder.cs @@ -19,19 +19,18 @@ public class IncludeClauseBuilder : QueryClauseBuilder private readonly Expression _source; private readonly ResourceContext _resourceContext; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; - public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, ResourceContext resourceContext, - IResourceContextProvider resourceContextProvider) + public IncludeClauseBuilder(Expression source, LambdaScope lambdaScope, ResourceContext resourceContext, IResourceGraph resourceGraph) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _source = source; _resourceContext = resourceContext; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public Expression ApplyInclude(IncludeExpression include) @@ -60,9 +59,9 @@ private Expression ProcessRelationshipChain(ResourceFieldChainExpression chain, foreach (RelationshipAttribute relationship in chain.Fields.Cast()) { - path = path == null ? relationship.Property.Name : path + "." + relationship.Property.Name; + path = path == null ? relationship.Property.Name : $"{path}.{relationship.Property.Name}"; - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(relationship.RightType); result = ApplyEagerLoads(result, resourceContext.EagerLoads, path); } @@ -75,7 +74,7 @@ private Expression ApplyEagerLoads(Expression source, IEnumerable private readonly Type _extensionType; private readonly LambdaParameterNameFactory _nameFactory; private readonly IResourceFactory _resourceFactory; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel entityModel, Type extensionType, LambdaParameterNameFactory nameFactory, - IResourceFactory resourceFactory, IResourceContextProvider resourceContextProvider) + IResourceFactory resourceFactory, IResourceGraph resourceGraph) : base(lambdaScope) { ArgumentGuard.NotNull(source, nameof(source)); @@ -39,14 +39,14 @@ public SelectClauseBuilder(Expression source, LambdaScope lambdaScope, IModel en ArgumentGuard.NotNull(extensionType, nameof(extensionType)); ArgumentGuard.NotNull(nameFactory, nameof(nameFactory)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); _source = source; _entityModel = entityModel; _extensionType = extensionType; _nameFactory = nameFactory; _resourceFactory = resourceFactory; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; } public Expression ApplySelect(IDictionary selectors, ResourceContext resourceContext) @@ -180,7 +180,7 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property { MemberExpression propertyExpression = Expression.Property(lambdaScope.Accessor, collectionProperty); - var builder = new QueryableBuilder(propertyExpression, elementType, typeof(Enumerable), _nameFactory, _resourceFactory, _resourceContextProvider, + var builder = new QueryableBuilder(propertyExpression, elementType, typeof(Enumerable), _nameFactory, _resourceFactory, _resourceGraph, _entityModel, lambdaScopeFactory); Expression layerExpression = builder.ApplyQuery(layer); @@ -221,7 +221,7 @@ public PropertySelector(PropertyInfo property, QueryLayer nextLayer = null) public override string ToString() { - return "Property: " + (NextLayer != null ? Property.Name + "..." : Property.Name); + return $"Property: {(NextLayer != null ? $"{Property.Name}..." : Property.Name)}"; } } } diff --git a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs index 66c336d674..573b19e4a4 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/SparseFieldSetCache.cs @@ -99,7 +99,7 @@ public IImmutableSet GetIdAttributeSetForRelationshipQuery(Resour { ArgumentGuard.NotNull(resourceContext, nameof(resourceContext)); - AttrAttribute idAttribute = resourceContext.Attributes.Single(attr => attr.Property.Name == nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); var inputExpression = new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); // Intentionally not cached, as we are fetching ID only (ignoring any sparse fieldset that came from query string). @@ -146,10 +146,7 @@ private IImmutableSet GetResourceFields(ResourceContext fieldSetBuilder.Add(attribute); } - foreach (RelationshipAttribute relationship in resourceContext.Relationships) - { - fieldSetBuilder.Add(relationship); - } + fieldSetBuilder.AddRange(resourceContext.Relationships); return fieldSetBuilder.ToImmutable(); } diff --git a/src/JsonApiDotNetCore/Queries/QueryLayer.cs b/src/JsonApiDotNetCore/Queries/QueryLayer.cs index 1ea0f62733..9340181743 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayer.cs @@ -41,7 +41,7 @@ public override string ToString() private static void WriteLayer(IndentingStringWriter writer, QueryLayer layer, string prefix = null) { - writer.WriteLine(prefix + nameof(QueryLayer) + "<" + layer.ResourceContext.ResourceType.Name + ">"); + writer.WriteLine($"{prefix}{nameof(QueryLayer)}<{layer.ResourceContext.ResourceType.Name}>"); using (writer.Indent()) { @@ -79,7 +79,7 @@ private static void WriteLayer(IndentingStringWriter writer, QueryLayer layer, s } else { - WriteLayer(writer, nextLayer, field.PublicName + ": "); + WriteLayer(writer, nextLayer, $"{field.PublicName}: "); } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs deleted file mode 100644 index 176bf69bda..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/IDefaultsQueryStringParameterReader.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings -{ - /// - /// Reads the 'defaults' query string parameter. - /// - public interface IDefaultsQueryStringParameterReader : IQueryStringParameterReader - { - /// - /// Contains the effective value of default configuration and query string override, after parsing has occurred. - /// - DefaultValueHandling SerializerDefaultValueHandling { get; } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs deleted file mode 100644 index e1885925e5..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/INullsQueryStringParameterReader.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings -{ - /// - /// Reads the 'nulls' query string parameter. - /// - public interface INullsQueryStringParameterReader : IQueryStringParameterReader - { - /// - /// Contains the effective value of default configuration and query string override, after parsing has occurred. - /// - NullValueHandling SerializerNullValueHandling { get; } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs deleted file mode 100644 index a58a16fdd3..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/DefaultsQueryStringParameterReader.cs +++ /dev/null @@ -1,54 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using Microsoft.Extensions.Primitives; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings.Internal -{ - /// - [PublicAPI] - public class DefaultsQueryStringParameterReader : IDefaultsQueryStringParameterReader - { - private readonly IJsonApiOptions _options; - - /// - public DefaultValueHandling SerializerDefaultValueHandling { get; private set; } - - public DefaultsQueryStringParameterReader(IJsonApiOptions options) - { - ArgumentGuard.NotNull(options, nameof(options)); - - _options = options; - SerializerDefaultValueHandling = options.SerializerSettings.DefaultValueHandling; - } - - /// - public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) - { - ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - - return _options.AllowQueryStringOverrideForSerializerDefaultValueHandling && - !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Defaults); - } - - /// - public virtual bool CanRead(string parameterName) - { - return parameterName == "defaults"; - } - - /// - public virtual void Read(string parameterName, StringValues parameterValue) - { - if (!bool.TryParse(parameterValue, out bool result)) - { - throw new InvalidQueryStringParameterException(parameterName, "The specified defaults is invalid.", - $"The value '{parameterValue}' must be 'true' or 'false'."); - } - - SerializerDefaultValueHandling = result ? DefaultValueHandling.Include : DefaultValueHandling.Ignore; - } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs index 899363178b..354cb4b8ec 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/FilterQueryStringParameterReader.cs @@ -29,15 +29,15 @@ public class FilterQueryStringParameterReader : QueryStringParameterReader, IFil private string _lastParameterName; - public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, + public FilterQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) - : base(request, resourceContextProvider) + : base(request, resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); - _filterParser = new FilterParser(resourceContextProvider, resourceFactory, ValidateSingleField); + _scopeParser = new QueryStringParameterScopeParser(resourceGraph, FieldChainRequirements.EndsInToMany); + _filterParser = new FilterParser(resourceGraph, resourceFactory, ValidateSingleField); } protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs index afdabbfbe9..2bed425170 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/IncludeQueryStringParameterReader.cs @@ -21,13 +21,13 @@ public class IncludeQueryStringParameterReader : QueryStringParameterReader, IIn private IncludeExpression _includeExpression; private string _lastParameterName; - public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - : base(request, resourceContextProvider) + public IncludeQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IJsonApiOptions options) + : base(request, resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _includeParser = new IncludeParser(resourceContextProvider, ValidateSingleRelationship); + _includeParser = new IncludeParser(resourceGraph, ValidateSingleRelationship); } protected void ValidateSingleRelationship(RelationshipAttribute relationship, ResourceContext resourceContext, string path) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs index e65e52a8fd..d3cfaec888 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/LegacyFilterNotationConverter.cs @@ -82,7 +82,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(InPrefix, StringComparison.Ordinal)) { string[] valueParts = parameterValue.Substring(InPrefix.Length).Split(","); - string valueList = "'" + string.Join("','", valueParts) + "'"; + string valueList = $"'{string.Join("','", valueParts)}'"; string expression = $"{Keywords.Any}({attributeName},{valueList})"; return (OutputParameterName, expression); @@ -91,7 +91,7 @@ public IEnumerable ExtractConditions(string parameterValue) if (parameterValue.StartsWith(NotInPrefix, StringComparison.Ordinal)) { string[] valueParts = parameterValue.Substring(NotInPrefix.Length).Split(","); - string valueList = "'" + string.Join("','", valueParts) + "'"; + string valueList = $"'{string.Join("','", valueParts)}'"; string expression = $"{Keywords.Not}({Keywords.Any}({attributeName},{valueList}))"; return (OutputParameterName, expression); diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs deleted file mode 100644 index aa40afbf25..0000000000 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/NullsQueryStringParameterReader.cs +++ /dev/null @@ -1,54 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using Microsoft.Extensions.Primitives; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.QueryStrings.Internal -{ - /// - [PublicAPI] - public class NullsQueryStringParameterReader : INullsQueryStringParameterReader - { - private readonly IJsonApiOptions _options; - - /// - public NullValueHandling SerializerNullValueHandling { get; private set; } - - public NullsQueryStringParameterReader(IJsonApiOptions options) - { - ArgumentGuard.NotNull(options, nameof(options)); - - _options = options; - SerializerNullValueHandling = options.SerializerSettings.NullValueHandling; - } - - /// - public virtual bool IsEnabled(DisableQueryStringAttribute disableQueryStringAttribute) - { - ArgumentGuard.NotNull(disableQueryStringAttribute, nameof(disableQueryStringAttribute)); - - return _options.AllowQueryStringOverrideForSerializerNullValueHandling && - !disableQueryStringAttribute.ContainsParameter(JsonApiQueryStringParameters.Nulls); - } - - /// - public virtual bool CanRead(string parameterName) - { - return parameterName == "nulls"; - } - - /// - public virtual void Read(string parameterName, StringValues parameterValue) - { - if (!bool.TryParse(parameterValue, out bool result)) - { - throw new InvalidQueryStringParameterException(parameterName, "The specified nulls is invalid.", - $"The value '{parameterValue}' must be 'true' or 'false'."); - } - - SerializerNullValueHandling = result ? NullValueHandling.Include : NullValueHandling.Ignore; - } - } -} diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs index ae6d8c4e67..47c6ec595e 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/PaginationQueryStringParameterReader.cs @@ -25,13 +25,13 @@ public class PaginationQueryStringParameterReader : QueryStringParameterReader, private PaginationQueryStringValueExpression _pageSizeConstraint; private PaginationQueryStringValueExpression _pageNumberConstraint; - public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider, IJsonApiOptions options) - : base(request, resourceContextProvider) + public PaginationQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph, IJsonApiOptions options) + : base(request, resourceGraph) { ArgumentGuard.NotNull(options, nameof(options)); _options = options; - _paginationParser = new PaginationParser(resourceContextProvider); + _paginationParser = new PaginationParser(resourceGraph); } /// diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs index e6fb1226da..b026ae7587 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringParameterReader.cs @@ -9,18 +9,18 @@ namespace JsonApiDotNetCore.QueryStrings.Internal { public abstract class QueryStringParameterReader { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly bool _isCollectionRequest; protected ResourceContext RequestResource { get; } protected bool IsAtomicOperationsRequest { get; } - protected QueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) + protected QueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) { ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _isCollectionRequest = request.IsCollection; RequestResource = request.SecondaryResource ?? request.PrimaryResource; IsAtomicOperationsRequest = request.Kind == EndpointKind.AtomicOperations; @@ -36,7 +36,7 @@ protected ResourceContext GetResourceContextForScope(ResourceFieldChainExpressio ResourceFieldAttribute lastField = scope.Fields[^1]; Type type = lastField is RelationshipAttribute relationship ? relationship.RightType : lastField.Property.PropertyType; - return _resourceContextProvider.GetResourceContext(type); + return _resourceGraph.GetResourceContext(type); } protected void AssertIsCollectionRequest() diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs index 68f9cc4a6c..28aabb53e5 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs @@ -68,8 +68,8 @@ public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttrib else if (!_options.AllowUnknownQueryStringParameters) { throw new InvalidQueryStringParameterException(parameterName, "Unknown query string parameter.", - $"Query string parameter '{parameterName}' is unknown. " + - $"Set '{nameof(IJsonApiOptions.AllowUnknownQueryStringParameters)}' to 'true' in options to ignore unknown parameters."); + $"Query string parameter '{parameterName}' is unknown. Set '{nameof(IJsonApiOptions.AllowUnknownQueryStringParameters)}' " + + "to 'true' in options to ignore unknown parameters."); } } } diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs index d96efbb4f5..e1ca5e0cd8 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SortQueryStringParameterReader.cs @@ -21,11 +21,11 @@ public class SortQueryStringParameterReader : QueryStringParameterReader, ISortQ private readonly List _constraints = new(); private string _lastParameterName; - public SortQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) - : base(request, resourceContextProvider) + public SortQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) + : base(request, resourceGraph) { - _scopeParser = new QueryStringParameterScopeParser(resourceContextProvider, FieldChainRequirements.EndsInToMany); - _sortParser = new SortParser(resourceContextProvider, ValidateSingleField); + _scopeParser = new QueryStringParameterScopeParser(resourceGraph, FieldChainRequirements.EndsInToMany); + _sortParser = new SortParser(resourceGraph, ValidateSingleField); } protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs index 08b8fa902f..096b31a7a1 100644 --- a/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs +++ b/src/JsonApiDotNetCore/QueryStrings/Internal/SparseFieldSetQueryStringParameterReader.cs @@ -30,11 +30,11 @@ public class SparseFieldSetQueryStringParameterReader : QueryStringParameterRead /// bool IQueryStringParameterReader.AllowEmptyValue => true; - public SparseFieldSetQueryStringParameterReader(IJsonApiRequest request, IResourceContextProvider resourceContextProvider) - : base(request, resourceContextProvider) + public SparseFieldSetQueryStringParameterReader(IJsonApiRequest request, IResourceGraph resourceGraph) + : base(request, resourceGraph) { - _sparseFieldTypeParser = new SparseFieldTypeParser(resourceContextProvider); - _sparseFieldSetParser = new SparseFieldSetParser(resourceContextProvider, ValidateSingleField); + _sparseFieldTypeParser = new SparseFieldTypeParser(resourceGraph); + _sparseFieldSetParser = new SparseFieldSetParser(resourceGraph, ValidateSingleField); } protected void ValidateSingleField(ResourceFieldAttribute field, ResourceContext resourceContext, string path) @@ -92,7 +92,7 @@ private SparseFieldSetExpression GetSparseFieldSet(string parameterValue, Resour if (sparseFieldSet == null) { // We add ID on an incoming empty fieldset, so that callers can distinguish between no fieldset and an empty one. - AttrAttribute idAttribute = resourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Identifiable.Id)); + AttrAttribute idAttribute = resourceContext.GetAttributeByPropertyName(nameof(Identifiable.Id)); return new SparseFieldSetExpression(ImmutableHashSet.Create(idAttribute)); } diff --git a/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs b/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs index 95406014ef..9c220686ac 100644 --- a/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs +++ b/src/JsonApiDotNetCore/QueryStrings/JsonApiQueryStringParameters.cs @@ -15,8 +15,6 @@ public enum JsonApiQueryStringParameters Include = 4, Page = 8, Fields = 16, - Nulls = 32, - Defaults = 64, - All = Filter | Sort | Include | Page | Fields | Nulls | Defaults + All = Filter | Sort | Include | Page | Fields } } diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index c23ca04fd1..d1ccdb418d 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -18,17 +18,17 @@ namespace JsonApiDotNetCore.Repositories public class ResourceRepositoryAccessor : IResourceRepositoryAccessor { private readonly IServiceProvider _serviceProvider; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; - public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceContextProvider resourceContextProvider, IJsonApiRequest request) + public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceGraph resourceGraph, IJsonApiRequest request) { ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); _serviceProvider = serviceProvider; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _request = request; } @@ -124,7 +124,7 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftRes protected virtual object ResolveReadRepository(Type resourceType) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { @@ -149,7 +149,7 @@ private object GetWriteRepository(Type resourceType) { if (writeRepository is not IRepositorySupportsTransaction repository) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); throw new MissingTransactionSupportException(resourceContext.PublicName); } @@ -164,7 +164,7 @@ private object GetWriteRepository(Type resourceType) protected virtual object ResolveWriteRepository(Type resourceType) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { diff --git a/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs b/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs index b05acd5eba..f7aaad9192 100644 --- a/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs +++ b/src/JsonApiDotNetCore/Resources/Internal/RuntimeTypeConverter.cs @@ -16,7 +16,8 @@ public static object ConvertType(object value, Type type) { if (!CanContainNull(type)) { - throw new FormatException($"Failed to convert 'null' to type '{type.Name}'."); + string targetTypeName = type.GetFriendlyTypeName(); + throw new FormatException($"Failed to convert 'null' to type '{targetTypeName}'."); } return null; @@ -73,7 +74,10 @@ public static object ConvertType(object value, Type type) catch (Exception exception) when (exception is FormatException || exception is OverflowException || exception is InvalidCastException || exception is ArgumentException) { - throw new FormatException($"Failed to convert '{value}' of type '{runtimeType.Name}' to type '{type.Name}'.", exception); + string runtimeTypeName = runtimeType.GetFriendlyTypeName(); + string targetTypeName = type.GetFriendlyTypeName(); + + throw new FormatException($"Failed to convert '{value}' of type '{runtimeTypeName}' to type '{targetTypeName}'.", exception); } } diff --git a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs index dc1aeafb91..b29fe33ef1 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceChangeTracker.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Resources { @@ -11,22 +11,19 @@ namespace JsonApiDotNetCore.Resources public sealed class ResourceChangeTracker : IResourceChangeTracker where TResource : class, IIdentifiable { - private readonly IJsonApiOptions _options; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly ITargetedFields _targetedFields; private IDictionary _initiallyStoredAttributeValues; private IDictionary _requestAttributeValues; private IDictionary _finallyStoredAttributeValues; - public ResourceChangeTracker(IJsonApiOptions options, IResourceContextProvider resourceContextProvider, ITargetedFields targetedFields) + public ResourceChangeTracker(IResourceGraph resourceGraph, ITargetedFields targetedFields) { - ArgumentGuard.NotNull(options, nameof(options)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); - _options = options; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _targetedFields = targetedFields; } @@ -35,7 +32,7 @@ public void SetInitiallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); _initiallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } @@ -52,7 +49,7 @@ public void SetFinallyStoredAttributeValues(TResource resource) { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(); _finallyStoredAttributeValues = CreateAttributeDictionary(resource, resourceContext.Attributes); } @@ -63,7 +60,7 @@ private IDictionary CreateAttributeDictionary(TResource resource foreach (AttrAttribute attribute in attributes) { object value = attribute.GetValue(resource); - string json = JsonConvert.SerializeObject(value, _options.SerializerSettings); + string json = JsonSerializer.Serialize(value); result.Add(attribute.PublicName, json); } diff --git a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs index 271f14ad5b..1923c33156 100644 --- a/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs +++ b/src/JsonApiDotNetCore/Resources/ResourceDefinitionAccessor.cs @@ -16,15 +16,15 @@ namespace JsonApiDotNetCore.Resources [PublicAPI] public class ResourceDefinitionAccessor : IResourceDefinitionAccessor { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IServiceProvider _serviceProvider; - public ResourceDefinitionAccessor(IResourceContextProvider resourceContextProvider, IServiceProvider serviceProvider) + public ResourceDefinitionAccessor(IResourceGraph resourceGraph, IServiceProvider serviceProvider) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(serviceProvider, nameof(serviceProvider)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _serviceProvider = serviceProvider; } @@ -194,7 +194,7 @@ public void OnSerialize(IIdentifiable resource) protected virtual object ResolveResourceDefinition(Type resourceType) { - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); if (resourceContext.IdentityType == typeof(int)) { diff --git a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs index 9f22dcbe3f..a7892755c5 100644 --- a/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/AtomicOperationsResponseSerializer.cs @@ -59,7 +59,7 @@ public string Serialize(object content) return SerializeOperationsDocument(operations); } - if (content is ErrorDocument errorDocument) + if (content is Document errorDocument) { return SerializeErrorDocument(errorDocument); } @@ -69,12 +69,19 @@ public string Serialize(object content) private string SerializeOperationsDocument(IEnumerable operations) { - var document = new AtomicOperationsDocument + var document = new Document { Results = operations.Select(SerializeOperation).ToList(), Meta = _metaBuilder.Build() }; + SetApiVersion(document); + + return SerializeObject(document, _options.SerializerWriteOptions); + } + + private void SetApiVersion(Document document) + { if (_options.IncludeJsonApiVersion) { document.JsonApi = new JsonApiObject @@ -86,8 +93,6 @@ private string SerializeOperationsDocument(IEnumerable opera } }; } - - return SerializeObject(document, _options.SerializerSettings); } private AtomicResultObject SerializeOperation(OperationContainer operation) @@ -116,16 +121,15 @@ private AtomicResultObject SerializeOperation(OperationContainer operation) return new AtomicResultObject { - Data = resourceObject + Data = new SingleOrManyData(resourceObject) }; } - private string SerializeErrorDocument(ErrorDocument errorDocument) + private string SerializeErrorDocument(Document document) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => - { - serializer.ApplyErrorSettings(); - }); + SetApiVersion(document); + + return SerializeObject(document, _options.SerializerWriteOptions); } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs index b329392c9f..dfba94691c 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs @@ -1,18 +1,13 @@ -using System; using System.Collections; using System.Collections.Generic; -using System.IO; using System.Linq; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Resources.Internal; -using JsonApiDotNetCore.Serialization.Client.Internal; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization { @@ -25,18 +20,16 @@ public abstract class BaseDeserializer { private protected static readonly CollectionConverter CollectionConverter = new(); - protected IResourceContextProvider ResourceContextProvider { get; } + protected IResourceGraph ResourceGraph { get; } protected IResourceFactory ResourceFactory { get; } - protected Document Document { get; set; } - protected int? AtomicOperationIndex { get; set; } - protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) + protected BaseDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(resourceFactory, nameof(resourceFactory)); - ResourceContextProvider = resourceContextProvider; + ResourceGraph = resourceGraph; ResourceFactory = resourceFactory; } @@ -45,7 +38,7 @@ protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IRe /// depending on the type of deserializer. /// /// - /// See the implementation of this method in and for examples. + /// See the implementation of this method in for usage. /// /// /// The resource that was constructed from the document's body. @@ -56,33 +49,47 @@ protected BaseDeserializer(IResourceContextProvider resourceContextProvider, IRe /// /// Relationship data for . Is null when is not a . /// - protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null); + protected abstract void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null); - protected object DeserializeBody(string body) + protected Document DeserializeDocument(string body, JsonSerializerOptions serializerOptions) { ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) + try { - JToken bodyJToken = LoadJToken(body); - Document = bodyJToken.ToObject(); + using (CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) + { + return JsonSerializer.Deserialize(body, serializerOptions); + } } + catch (JsonException exception) + { + // JsonException.Path looks great for setting error.source.pointer, but unfortunately it is wrong in most cases. + // This is due to the use of custom converters, which are unable to interact with internal position tracking. + // https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245 + throw new JsonApiSerializationException(null, exception.Message, exception); + } + } + + protected object DeserializeData(string body, JsonSerializerOptions serializerOptions) + { + Document document = DeserializeDocument(body, serializerOptions); - if (Document != null) + if (document != null) { - if (Document.IsManyData) + if (document.Data.ManyValue != null) { using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (list)")) { - return Document.ManyData.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); + return document.Data.ManyValue.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance); } } - if (Document.SingleData != null) + if (document.Data.SingleValue != null) { using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (single)")) { - return ParseResourceObject(Document.SingleData); + return ParseResourceObject(document.Data.SingleValue); } } } @@ -102,8 +109,7 @@ protected object DeserializeBody(string body) /// /// Exposed attributes for . /// - protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, - IReadOnlyCollection attributes) + private IIdentifiable SetAttributes(IIdentifiable resource, IDictionary attributeValues, IReadOnlyCollection attributes) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(attributes, nameof(attributes)); @@ -123,8 +129,21 @@ protected IIdentifiable SetAttributes(IIdentifiable resource, IDictionary /// Exposed relationships for . /// - protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, + private IIdentifiable SetRelationships(IIdentifiable resource, IDictionary relationshipValues, IReadOnlyCollection relationshipAttributes) { ArgumentGuard.NotNull(resource, nameof(resource)); @@ -157,9 +176,9 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio foreach (RelationshipAttribute attr in relationshipAttributes) { - bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipEntry relationshipData); + bool relationshipIsProvided = relationshipValues.TryGetValue(attr.PublicName, out RelationshipObject relationshipData); - if (!relationshipIsProvided || !relationshipData.IsPopulated) + if (!relationshipIsProvided || !relationshipData.Data.IsAssigned) { continue; } @@ -177,19 +196,6 @@ protected virtual IIdentifiable SetRelationships(IIdentifiable resource, IDictio return resource; } -#pragma warning disable AV1130 // Return type in method signature should be a collection interface instead of a concrete type - protected JToken LoadJToken(string body) -#pragma warning restore AV1130 // Return type in method signature should be a collection interface instead of a concrete type - { - using JsonReader jsonReader = new JsonTextReader(new StringReader(body)) - { - // https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/509 - DateParseHandling = DateParseHandling.None - }; - - return JToken.Load(jsonReader); - } - /// /// Creates an instance of the referenced type in and sets its attributes and relationships. /// @@ -223,7 +229,7 @@ protected IIdentifiable ParseResourceObject(ResourceObject data) protected ResourceContext GetExistingResourceContext(string publicName) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(publicName); + ResourceContext resourceContext = ResourceGraph.TryGetResourceContext(publicName); if (resourceContext == null) { @@ -237,15 +243,15 @@ protected ResourceContext GetExistingResourceContext(string publicName) /// /// Sets a HasOne relationship on a parsed resource. /// - private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOneRelationship, RelationshipEntry relationshipData) + private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOneRelationship, RelationshipObject relationshipData) { - if (relationshipData.ManyData != null) + if (relationshipData.Data.ManyValue != null) { throw new JsonApiSerializationException("Expected single data element for to-one relationship.", $"Expected single data element for '{hasOneRelationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - IIdentifiable rightResource = CreateRightResource(hasOneRelationship, relationshipData.SingleData); + IIdentifiable rightResource = CreateRightResource(hasOneRelationship, relationshipData.Data.SingleValue); hasOneRelationship.SetValue(resource, rightResource); // depending on if this base parser is used client-side or server-side, @@ -256,15 +262,15 @@ private void SetHasOneRelationship(IIdentifiable resource, HasOneAttribute hasOn /// /// Sets a HasMany relationship. /// - private void SetHasManyRelationship(IIdentifiable resource, HasManyAttribute hasManyRelationship, RelationshipEntry relationshipData) + private void SetHasManyRelationship(IIdentifiable resource, HasManyAttribute hasManyRelationship, RelationshipObject relationshipData) { - if (relationshipData.ManyData == null) + if (relationshipData.Data.ManyValue == null) { throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", $"Expected data[] element for '{hasManyRelationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - HashSet rightResources = relationshipData.ManyData.Select(rio => CreateRightResource(hasManyRelationship, rio)) + HashSet rightResources = relationshipData.Data.ManyValue.Select(rio => CreateRightResource(hasManyRelationship, rio)) .ToHashSet(IdentifiableComparer.Instance); IEnumerable convertedCollection = CollectionConverter.CopyToTypedCollection(rightResources, hasManyRelationship.Property.PropertyType); @@ -294,9 +300,9 @@ private IIdentifiable CreateRightResource(RelationshipAttribute relationship, Re } [AssertionMethod] - private void AssertHasType(ResourceIdentifierObject resourceIdentifierObject, RelationshipAttribute relationship) + private void AssertHasType(IResourceIdentity resourceIdentity, RelationshipAttribute relationship) { - if (resourceIdentifierObject.Type == null) + if (resourceIdentity.Type == null) { string details = relationship != null ? $"Expected 'type' element in '{relationship.PublicName}' relationship." @@ -332,11 +338,11 @@ private void AssertHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, } [AssertionMethod] - private void AssertHasNoLid(ResourceIdentifierObject resourceIdentifierObject) + private void AssertHasNoLid(IResourceIdentity resourceIdentityObject) { - if (resourceIdentifierObject.Lid != null) + if (resourceIdentityObject.Lid != null) { - throw new JsonApiSerializationException("Local IDs cannot be used at this endpoint.", null, atomicOperationIndex: AtomicOperationIndex); + throw new JsonApiSerializationException(null, "Local IDs cannot be used at this endpoint.", atomicOperationIndex: AtomicOperationIndex); } } @@ -349,23 +355,5 @@ private void AssertRightTypeIsCompatible(ResourceContext rightResourceContext, R atomicOperationIndex: AtomicOperationIndex); } } - - private object ConvertAttrValue(object newValue, Type targetType) - { - if (newValue is JContainer jObject) - { - // the attribute value is a complex type that needs additional deserialization - return DeserializeComplexType(jObject, targetType); - } - - // the attribute value is a native C# type. - object convertedValue = RuntimeTypeConverter.ConvertType(newValue, targetType); - return convertedValue; - } - - private object DeserializeComplexType(JContainer obj, Type targetType) - { - return obj.ToObject(targetType); - } } } diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs index 8519d20fd2..d096a4ea6c 100644 --- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs @@ -1,12 +1,11 @@ using System; using System.Collections.Generic; -using System.IO; +using System.Text.Json; using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -46,14 +45,11 @@ protected Document Build(IIdentifiable resource, IReadOnlyCollection(resourceObject) }; } @@ -80,33 +76,27 @@ protected Document Build(IReadOnlyCollection resources, IReadOnly using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (list)"); - var data = new List(); + var resourceObjects = new List(); foreach (IIdentifiable resource in resources) { - data.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); + resourceObjects.Add(ResourceObjectBuilder.Build(resource, attributes, relationships)); } return new Document { - Data = data + Data = new SingleOrManyData(resourceObjects) }; } - protected string SerializeObject(object value, JsonSerializerSettings defaultSettings, Action changeSerializer = null) + protected string SerializeObject(object value, JsonSerializerOptions serializerOptions) { - ArgumentGuard.NotNull(defaultSettings, nameof(defaultSettings)); - - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Newtonsoft.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - - var serializer = JsonSerializer.CreateDefault(defaultSettings); - changeSerializer?.Invoke(serializer); + ArgumentGuard.NotNull(serializerOptions, nameof(serializerOptions)); - using var stringWriter = new StringWriter(); - using var jsonWriter = new JsonTextWriter(stringWriter); + using IDisposable _ = + CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - serializer.Serialize(jsonWriter, value); - return stringWriter.ToString(); + return JsonSerializer.Serialize(value, serializerOptions); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs deleted file mode 100644 index 0fcaefd32a..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/IResourceObjectBuilderSettingsProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace JsonApiDotNetCore.Serialization.Building -{ - /// - /// Service that provides the server serializer with . - /// - public interface IResourceObjectBuilderSettingsProvider - { - /// - /// Gets the behavior for the serializer it is injected in. - /// - ResourceObjectBuilderSettings Get(); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs index 4ba4a3f9f1..299c270f91 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/IncludedResourceObjectBuilder.cs @@ -24,10 +24,10 @@ public class IncludedResourceObjectBuilder : ResourceObjectBuilder, IIncludedRes private readonly IRequestQueryStringAccessor _queryStringAccessor; private readonly SparseFieldSetCache _sparseFieldSetCache; - public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceContextProvider resourceContextProvider, + public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILinkBuilder linkBuilder, IResourceGraph resourceGraph, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, - IRequestQueryStringAccessor queryStringAccessor, IResourceObjectBuilderSettingsProvider settingsProvider) - : base(resourceContextProvider, settingsProvider.Get()) + IRequestQueryStringAccessor queryStringAccessor, IJsonApiOptions options) + : base(resourceGraph, options) { ArgumentGuard.NotNull(fieldsToSerialize, nameof(fieldsToSerialize)); ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); @@ -35,7 +35,7 @@ public IncludedResourceObjectBuilder(IFieldsToSerialize fieldsToSerialize, ILink ArgumentGuard.NotNull(resourceDefinitionAccessor, nameof(resourceDefinitionAccessor)); ArgumentGuard.NotNull(queryStringAccessor, nameof(queryStringAccessor)); - _included = new HashSet(ResourceIdentifierObjectComparer.Instance); + _included = new HashSet(ResourceIdentityComparer.Instance); _fieldsToSerialize = fieldsToSerialize; _linkBuilder = linkBuilder; _resourceDefinitionAccessor = resourceDefinitionAccessor; @@ -69,8 +69,8 @@ private void UpdateRelationships(ResourceObject resourceObject) { foreach (string relationshipName in resourceObject.Relationships.Keys) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resourceObject.Type); - RelationshipAttribute relationship = resourceContext.Relationships.Single(rel => rel.PublicName == relationshipName); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(resourceObject.Type); + RelationshipAttribute relationship = resourceContext.GetRelationshipByPublicName(relationshipName); if (!IsRelationshipInSparseFieldSet(relationship)) { @@ -78,12 +78,12 @@ private void UpdateRelationships(ResourceObject resourceObject) } } - resourceObject.Relationships = PruneRelationshipEntries(resourceObject); + resourceObject.Relationships = PruneRelationshipObjects(resourceObject); } - private static IDictionary PruneRelationshipEntries(ResourceObject resourceObject) + private static IDictionary PruneRelationshipObjects(ResourceObject resourceObject) { - Dictionary pruned = resourceObject.Relationships.Where(pair => pair.Value.IsPopulated || pair.Value.Links != null) + Dictionary pruned = resourceObject.Relationships.Where(pair => pair.Value.Data.IsAssigned || pair.Value.Links != null) .ToDictionary(pair => pair.Key, pair => pair.Value); return !pruned.Any() ? null : pruned; @@ -91,7 +91,7 @@ private static IDictionary PruneRelationshipEntries(R private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(relationship.LeftType); IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); @@ -140,6 +140,11 @@ private void ProcessChain(object related, IList inclusion private void ProcessRelationship(IIdentifiable parent, IList inclusionChain) { + if (parent == null) + { + return; + } + ResourceObject resourceObject = TryGetBuiltResourceObjectFor(parent); if (resourceObject == null) @@ -159,18 +164,17 @@ private void ProcessRelationship(IIdentifiable parent, IList relationshipsObject = resourceObject.Relationships; + IDictionary relationshipsObject = resourceObject.Relationships; - // add the relationship entry in the relationship object. - if (!relationshipsObject.TryGetValue(nextRelationshipName, out RelationshipEntry relationshipEntry)) + if (!relationshipsObject.TryGetValue(nextRelationshipName, out RelationshipObject relationshipObject)) { - relationshipEntry = GetRelationshipData(nextRelationship, parent); - relationshipsObject[nextRelationshipName] = relationshipEntry; + relationshipObject = GetRelationshipData(nextRelationship, parent); + relationshipsObject[nextRelationshipName] = relationshipObject; } - relationshipEntry.Data = GetRelatedResourceLinkage(nextRelationship, parent); + relationshipObject.Data = GetRelatedResourceLinkage(nextRelationship, parent); - if (relationshipEntry.HasResource) + if (relationshipObject.Data.IsAssigned && relationshipObject.Data.Value != null) { // if the relationship is set, continue parsing the chain. object related = nextRelationship.GetValue(parent); @@ -186,14 +190,14 @@ private IList ShiftChain(IReadOnlyCollection - /// We only need an empty relationship object entry here. It will be populated in the ProcessRelationships method. + /// We only need an empty relationship object here. It will be populated in the ProcessRelationships method. /// - protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) + protected override RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - return new RelationshipEntry + return new RelationshipObject { Links = _linkBuilder.GetRelationshipLinks(relationship, resource) }; @@ -202,7 +206,7 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r private ResourceObject TryGetBuiltResourceObjectFor(IIdentifiable resource) { Type resourceType = resource.GetType(); - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(resourceType); return _included.SingleOrDefault(resourceObject => resourceObject.Type == resourceContext.PublicName && resourceObject.Id == resource.StringId); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs index 3ffcedbdfe..ad82acff5b 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/LinkBuilder.cs @@ -31,26 +31,25 @@ public class LinkBuilder : ILinkBuilder private readonly IJsonApiOptions _options; private readonly IJsonApiRequest _request; private readonly IPaginationContext _paginationContext; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IHttpContextAccessor _httpContextAccessor; private readonly LinkGenerator _linkGenerator; private readonly IControllerResourceMapping _controllerResourceMapping; - public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, - IResourceContextProvider resourceContextProvider, IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator, - IControllerResourceMapping controllerResourceMapping) + public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IResourceGraph resourceGraph, + IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping) { ArgumentGuard.NotNull(options, nameof(options)); ArgumentGuard.NotNull(request, nameof(request)); ArgumentGuard.NotNull(paginationContext, nameof(paginationContext)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(linkGenerator, nameof(linkGenerator)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); _options = options; _request = request; _paginationContext = paginationContext; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _httpContextAccessor = httpContextAccessor; _linkGenerator = linkGenerator; _controllerResourceMapping = controllerResourceMapping; @@ -173,7 +172,7 @@ private IImmutableList ParsePageSiz return ImmutableArray.Empty; } - var parser = new PaginationParser(_resourceContextProvider); + var parser = new PaginationParser(_resourceGraph); PaginationQueryStringValueExpression paginationExpression = parser.Parse(pageSizeParameterValue, requestResource); return paginationExpression.Elements; @@ -231,7 +230,7 @@ public ResourceLinks GetResourceLinks(string resourceName, string id) ArgumentGuard.NotNullNorEmpty(id, nameof(id)); var links = new ResourceLinks(); - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceName); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceName); if (_request.Kind != EndpointKind.Relationship && ShouldIncludeResourceLink(LinkTypes.Self, resourceContext)) { @@ -270,7 +269,7 @@ public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship ArgumentGuard.NotNull(leftResource, nameof(leftResource)); var links = new RelationshipLinks(); - ResourceContext leftResourceContext = _resourceContextProvider.GetResourceContext(leftResource.GetType()); + ResourceContext leftResourceContext = _resourceGraph.GetResourceContext(leftResource.GetType()); if (ShouldIncludeRelationshipLink(LinkTypes.Self, relationship, leftResourceContext)) { diff --git a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs index d741e6af26..dcddb2aa53 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/MetaBuilder.cs @@ -40,7 +40,11 @@ public IDictionary Build() { if (_paginationContext.TotalResourceCount != null) { - string key = _options.SerializerNamingStrategy.GetPropertyName("TotalResources", false); + const string keyName = "Total"; + + string key = _options.SerializerOptions.DictionaryKeyPolicy == null + ? keyName + : _options.SerializerOptions.DictionaryKeyPolicy.ConvertName(keyName); _meta.Add(key, _paginationContext.TotalResourceCount); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs similarity index 60% rename from src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs rename to src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs index 65760804c2..722008815e 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentifierObjectComparer.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceIdentityComparer.cs @@ -4,15 +4,15 @@ namespace JsonApiDotNetCore.Serialization.Building { - internal sealed class ResourceIdentifierObjectComparer : IEqualityComparer + internal sealed class ResourceIdentityComparer : IEqualityComparer { - public static readonly ResourceIdentifierObjectComparer Instance = new(); + public static readonly ResourceIdentityComparer Instance = new(); - private ResourceIdentifierObjectComparer() + private ResourceIdentityComparer() { } - public bool Equals(ResourceIdentifierObject x, ResourceIdentifierObject y) + public bool Equals(IResourceIdentity x, IResourceIdentity y) { if (ReferenceEquals(x, y)) { @@ -27,7 +27,7 @@ public bool Equals(ResourceIdentifierObject x, ResourceIdentifierObject y) return x.Type == y.Type && x.Id == y.Id && x.Lid == y.Lid; } - public int GetHashCode(ResourceIdentifierObject obj) + public int GetHashCode(IResourceIdentity obj) { return HashCode.Combine(obj.Type, obj.Id, obj.Lid); } diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs index 787ce2a53b..6f78367108 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilder.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Resources.Internal; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization.Building { @@ -15,17 +15,17 @@ namespace JsonApiDotNetCore.Serialization.Building public class ResourceObjectBuilder : IResourceObjectBuilder { private static readonly CollectionConverter CollectionConverter = new(); + private readonly IJsonApiOptions _options; - private readonly ResourceObjectBuilderSettings _settings; - protected IResourceContextProvider ResourceContextProvider { get; } + protected IResourceGraph ResourceGraph { get; } - public ResourceObjectBuilder(IResourceContextProvider resourceContextProvider, ResourceObjectBuilderSettings settings) + public ResourceObjectBuilder(IResourceGraph resourceGraph, IJsonApiOptions options) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); - ArgumentGuard.NotNull(settings, nameof(settings)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(options, nameof(options)); - ResourceContextProvider = resourceContextProvider; - _settings = settings; + ResourceGraph = resourceGraph; + _options = options; } /// @@ -34,7 +34,7 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< { ArgumentGuard.NotNull(resource, nameof(resource)); - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(resource.GetType()); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(resource.GetType()); // populating the top-level "type" and "id" members. var resourceObject = new ResourceObject @@ -64,25 +64,24 @@ public virtual ResourceObject Build(IIdentifiable resource, IReadOnlyCollection< } /// - /// Builds the entries of the "relationships objects". The default behavior is to just construct a resource linkage with - /// the "data" field populated with "single" or "many" data. Depending on the requirements of the implementation (server or client serializer), this may - /// be overridden. + /// Builds a . The default behavior is to just construct a resource linkage with the "data" field populated with + /// "single" or "many" data. /// - protected virtual RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) + protected virtual RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - return new RelationshipEntry + return new RelationshipObject { Data = GetRelatedResourceLinkage(relationship, resource) }; } /// - /// Gets the value for the property. + /// Gets the value for the data property. /// - protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) + protected SingleOrManyData GetRelatedResourceLinkage(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); @@ -95,22 +94,17 @@ protected object GetRelatedResourceLinkage(RelationshipAttribute relationship, I /// /// Builds a for a HasOne relationship. /// - private ResourceIdentifierObject GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) + private SingleOrManyData GetRelatedResourceLinkageForHasOne(HasOneAttribute relationship, IIdentifiable resource) { var relatedResource = (IIdentifiable)relationship.GetValue(resource); - - if (relatedResource != null) - { - return GetResourceIdentifier(relatedResource); - } - - return null; + ResourceIdentifierObject resourceIdentifierObject = relatedResource != null ? GetResourceIdentifier(relatedResource) : null; + return new SingleOrManyData(resourceIdentifierObject); } /// /// Builds the s for a HasMany relationship. /// - private IList GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) + private SingleOrManyData GetRelatedResourceLinkageForHasMany(HasManyAttribute relationship, IIdentifiable resource) { object value = relationship.GetValue(resource); ICollection relatedResources = CollectionConverter.ExtractResources(value); @@ -125,7 +119,7 @@ private IList GetRelatedResourceLinkageForHasMany(HasM } } - return manyData; + return new SingleOrManyData(manyData); } /// @@ -133,7 +127,7 @@ private IList GetRelatedResourceLinkageForHasMany(HasM /// private ResourceIdentifierObject GetResourceIdentifier(IIdentifiable resource) { - string publicName = ResourceContextProvider.GetResourceContext(resource.GetType()).PublicName; + string publicName = ResourceGraph.GetResourceContext(resource.GetType()).PublicName; return new ResourceIdentifierObject { @@ -149,11 +143,11 @@ private void ProcessRelationships(IIdentifiable resource, IEnumerable()).Add(rel.PublicName, relData); + (ro.Relationships ??= new Dictionary()).Add(rel.PublicName, relData); } } } @@ -169,15 +163,15 @@ private void ProcessAttributes(IIdentifiable resource, IEnumerable - /// Options used to configure how fields of a model get serialized into a JSON:API . - /// - [PublicAPI] - public sealed class ResourceObjectBuilderSettings - { - public NullValueHandling SerializerNullValueHandling { get; } - public DefaultValueHandling SerializerDefaultValueHandling { get; } - - public ResourceObjectBuilderSettings(NullValueHandling serializerNullValueHandling = NullValueHandling.Include, - DefaultValueHandling serializerDefaultValueHandling = DefaultValueHandling.Include) - { - SerializerNullValueHandling = serializerNullValueHandling; - SerializerDefaultValueHandling = serializerDefaultValueHandling; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs deleted file mode 100644 index 04a5128aed..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Building/ResourceObjectBuilderSettingsProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.QueryStrings; - -namespace JsonApiDotNetCore.Serialization.Building -{ - /// - /// This implementation of the behavior provider reads the defaults/nulls query string parameters that can, if provided, override the settings in - /// . - /// - public sealed class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider - { - private readonly IDefaultsQueryStringParameterReader _defaultsReader; - private readonly INullsQueryStringParameterReader _nullsReader; - - public ResourceObjectBuilderSettingsProvider(IDefaultsQueryStringParameterReader defaultsReader, INullsQueryStringParameterReader nullsReader) - { - ArgumentGuard.NotNull(defaultsReader, nameof(defaultsReader)); - ArgumentGuard.NotNull(nullsReader, nameof(nullsReader)); - - _defaultsReader = defaultsReader; - _nullsReader = nullsReader; - } - - /// - public ResourceObjectBuilderSettings Get() - { - return new(_nullsReader.SerializerNullValueHandling, _defaultsReader.SerializerDefaultValueHandling); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs index e23eef6a82..7138b6a12b 100644 --- a/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Building/ResponseResourceObjectBuilder.cs @@ -26,10 +26,9 @@ public class ResponseResourceObjectBuilder : ResourceObjectBuilder private RelationshipAttribute _requestRelationship; public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResourceObjectBuilder includedBuilder, - IEnumerable constraintProviders, IResourceContextProvider resourceContextProvider, - IResourceDefinitionAccessor resourceDefinitionAccessor, IResourceObjectBuilderSettingsProvider settingsProvider, - IEvaluatedIncludeCache evaluatedIncludeCache) - : base(resourceContextProvider, settingsProvider.Get()) + IEnumerable constraintProviders, IResourceGraph resourceGraph, IResourceDefinitionAccessor resourceDefinitionAccessor, + IJsonApiOptions options, IEvaluatedIncludeCache evaluatedIncludeCache) + : base(resourceGraph, options) { ArgumentGuard.NotNull(linkBuilder, nameof(linkBuilder)); ArgumentGuard.NotNull(includedBuilder, nameof(includedBuilder)); @@ -44,7 +43,7 @@ public ResponseResourceObjectBuilder(ILinkBuilder linkBuilder, IIncludedResource _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } - public RelationshipEntry Build(IIdentifiable resource, RelationshipAttribute requestRelationship) + public RelationshipObject Build(IIdentifiable resource, RelationshipAttribute requestRelationship) { ArgumentGuard.NotNull(resource, nameof(resource)); ArgumentGuard.NotNull(requestRelationship, nameof(requestRelationship)); @@ -65,23 +64,24 @@ public override ResourceObject Build(IIdentifiable resource, IReadOnlyCollection } /// - /// Builds the values of the relationships object on a resource object. The server serializer only populates the "data" member when the relationship is - /// included, and adds links unless these are turned off. This means that if a relationship is not included and links are turned off, the entry would be - /// completely empty, ie { }, which is not conform JSON:API spec. In that case we return null which will omit the entry from the output. + /// Builds a for the specified relationship on a resource. The serializer only populates the "data" member when the + /// relationship is included, and adds links unless these are turned off. This means that if a relationship is not included and links are turned off, the + /// object would be completely empty, ie { }, which is not conform JSON:API spec. In that case we return null, which will omit the object from the + /// output. /// - protected override RelationshipEntry GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) + protected override RelationshipObject GetRelationshipData(RelationshipAttribute relationship, IIdentifiable resource) { ArgumentGuard.NotNull(relationship, nameof(relationship)); ArgumentGuard.NotNull(resource, nameof(resource)); - RelationshipEntry relationshipEntry = null; + RelationshipObject relationshipObject = null; IReadOnlyCollection> relationshipChains = GetInclusionChainsStartingWith(relationship); if (Equals(relationship, _requestRelationship) || relationshipChains.Any()) { - relationshipEntry = base.GetRelationshipData(relationship, resource); + relationshipObject = base.GetRelationshipData(relationship, resource); - if (relationshipChains.Any() && relationshipEntry.HasResource) + if (relationshipChains.Any() && relationshipObject.Data.Value != null) { foreach (IReadOnlyCollection chain in relationshipChains) { @@ -100,19 +100,19 @@ protected override RelationshipEntry GetRelationshipData(RelationshipAttribute r if (links != null) { - // if relationshipLinks should be built for this entry, populate the "links" field. - relationshipEntry ??= new RelationshipEntry(); - relationshipEntry.Links = links; + // if relationshipLinks should be built, populate the "links" field. + relationshipObject ??= new RelationshipObject(); + relationshipObject.Links = links; } - // if neither "links" nor "data" was populated, return null, which will omit this entry from the output. + // if neither "links" nor "data" was populated, return null, which will omit this object from the output. // (see the NullValueHandling settings on ) - return relationshipEntry; + return relationshipObject; } private bool IsRelationshipInSparseFieldSet(RelationshipAttribute relationship) { - ResourceContext resourceContext = ResourceContextProvider.GetResourceContext(relationship.LeftType); + ResourceContext resourceContext = ResourceGraph.GetResourceContext(relationship.LeftType); IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return fieldSet.Contains(relationship); diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs deleted file mode 100644 index f1051dfd47..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/DeserializedResponseBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Base class for "single data" and "many data" deserialized responses. - /// - [PublicAPI] - public abstract class DeserializedResponseBase - { - public TopLevelLinks Links { get; set; } - public IDictionary Meta { get; set; } - public object Errors { get; set; } - public object JsonApi { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs deleted file mode 100644 index c1252c06ad..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IRequestSerializer.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Interface for client serializer that can be used to register with the DI container, for usage in custom services or repositories. - /// - [PublicAPI] - public interface IRequestSerializer - { - /// - /// Sets the attributes that will be included in the serialized request body. You can use - /// to conveniently access the desired instances. - /// - public IReadOnlyCollection AttributesToSerialize { get; set; } - - /// - /// Sets the relationships that will be included in the serialized request body. You can use - /// to conveniently access the desired instances. - /// - public IReadOnlyCollection RelationshipsToSerialize { get; set; } - - /// - /// Creates and serializes a document for a single resource. - /// - /// - /// The serialized content - /// - string Serialize(IIdentifiable resource); - - /// - /// Creates and serializes a document for a collection of resources. - /// - /// - /// The serialized content - /// - string Serialize(IReadOnlyCollection resources); - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs deleted file mode 100644 index 0a41306523..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/IResponseDeserializer.cs +++ /dev/null @@ -1,37 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Client deserializer. Currently not used internally in JsonApiDotNetCore, except for in the tests. Exposed publicly to make testing easier or to - /// implement server-to-server communication. - /// - [PublicAPI] - public interface IResponseDeserializer - { - /// - /// Deserializes a response with a single resource (or null) as data. - /// - /// - /// The type of the resources in the primary data. - /// - /// - /// The JSON to be deserialized. - /// - SingleResponse DeserializeSingle(string body) - where TResource : class, IIdentifiable; - - /// - /// Deserializes a response with an (empty) collection of resources as data. - /// - /// - /// The type of the resources in the primary data. - /// - /// - /// The JSON to be deserialized. - /// - ManyResponse DeserializeMany(string body) - where TResource : class, IIdentifiable; - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs deleted file mode 100644 index 659e33d0dd..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ManyResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Represents a deserialized document with "many data". - /// - /// - /// Type of the resource(s) in the primary data. - /// - [PublicAPI] - public sealed class ManyResponse : DeserializedResponseBase - where TResource : class, IIdentifiable - { - public IReadOnlyCollection Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs deleted file mode 100644 index e0910b3a58..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/RequestSerializer.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Client serializer implementation of . - /// - [PublicAPI] - public class RequestSerializer : BaseSerializer, IRequestSerializer - { - private readonly IResourceGraph _resourceGraph; - private readonly JsonSerializerSettings _jsonSerializerSettings = new(); - private Type _currentTargetedResource; - - /// - public IReadOnlyCollection AttributesToSerialize { get; set; } - - /// - public IReadOnlyCollection RelationshipsToSerialize { get; set; } - - public RequestSerializer(IResourceGraph resourceGraph, IResourceObjectBuilder resourceObjectBuilder) - : base(resourceObjectBuilder) - { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); - - _resourceGraph = resourceGraph; - } - - /// - public string Serialize(IIdentifiable resource) - { - if (resource == null) - { - Document empty = Build((IIdentifiable)null, Array.Empty(), Array.Empty()); - return SerializeObject(empty, _jsonSerializerSettings); - } - - _currentTargetedResource = resource.GetType(); - Document document = Build(resource, GetAttributesToSerialize(resource), RelationshipsToSerialize); - _currentTargetedResource = null; - - return SerializeObject(document, _jsonSerializerSettings); - } - - /// - public string Serialize(IReadOnlyCollection resources) - { - ArgumentGuard.NotNull(resources, nameof(resources)); - - IIdentifiable firstResource = resources.FirstOrDefault(); - - Document document; - - if (firstResource == null) - { - document = Build(resources, Array.Empty(), Array.Empty()); - } - else - { - _currentTargetedResource = firstResource.GetType(); - IReadOnlyCollection attributes = GetAttributesToSerialize(firstResource); - - document = Build(resources, attributes, RelationshipsToSerialize); - _currentTargetedResource = null; - } - - return SerializeObject(document, _jsonSerializerSettings); - } - - /// - /// By default, the client serializer includes all attributes in the result, unless a list of allowed attributes was supplied using the - /// method. For any related resources, attributes are never exposed. - /// - private IReadOnlyCollection GetAttributesToSerialize(IIdentifiable resource) - { - Type currentResourceType = resource.GetType(); - - if (_currentTargetedResource != currentResourceType) - { - // We're dealing with a relationship that is being serialized, for which - // we never want to include any attributes in the request body. - return new List(); - } - - if (AttributesToSerialize == null) - { - ResourceContext resourceContext = _resourceGraph.GetResourceContext(currentResourceType); - return resourceContext.Attributes; - } - - return AttributesToSerialize; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs deleted file mode 100644 index a08c3655fd..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/ResponseDeserializer.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Client deserializer implementation of the . - /// - [PublicAPI] - public class ResponseDeserializer : BaseDeserializer, IResponseDeserializer - { - public ResponseDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory) - : base(resourceContextProvider, resourceFactory) - { - } - - /// - public SingleResponse DeserializeSingle(string body) - where TResource : class, IIdentifiable - { - ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - - object resource = DeserializeBody(body); - - return new SingleResponse - { - Links = Document.Links, - Meta = Document.Meta, - Data = (TResource)resource, - JsonApi = null, - Errors = null - }; - } - - /// - public ManyResponse DeserializeMany(string body) - where TResource : class, IIdentifiable - { - ArgumentGuard.NotNullNorEmpty(body, nameof(body)); - - object resources = DeserializeBody(body); - - return new ManyResponse - { - Links = Document.Links, - Meta = Document.Meta, - Data = ((ICollection)resources)?.Cast().ToArray(), - JsonApi = null, - Errors = null - }; - } - - /// - /// Additional processing required for client deserialization, responsible for parsing the property. When a relationship - /// value is parsed, it goes through the included list to set its attributes and relationships. - /// - /// - /// The resource that was constructed from the document's body. - /// - /// - /// The metadata for the exposed field. - /// - /// - /// Relationship data for . Is null when is not a . - /// - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) - { - ArgumentGuard.NotNull(resource, nameof(resource)); - ArgumentGuard.NotNull(field, nameof(field)); - - // Client deserializers do not need additional processing for attributes. - if (field is AttrAttribute) - { - return; - } - - // if the included property is empty or absent, there is no additional data to be parsed. - if (Document.Included.IsNullOrEmpty()) - { - return; - } - - if (data != null) - { - if (field is HasOneAttribute hasOneAttr) - { - // add attributes and relationships of a parsed HasOne relationship - ResourceIdentifierObject rio = data.SingleData; - hasOneAttr.SetValue(resource, rio == null ? null : ParseIncludedRelationship(rio)); - } - else if (field is HasManyAttribute hasManyAttr) - { - // add attributes and relationships of a parsed HasMany relationship - IEnumerable items = data.ManyData.Select(ParseIncludedRelationship); - IEnumerable values = CollectionConverter.CopyToTypedCollection(items, hasManyAttr.Property.PropertyType); - hasManyAttr.SetValue(resource, values); - } - } - } - - /// - /// Searches for and parses the included relationship. - /// - private IIdentifiable ParseIncludedRelationship(ResourceIdentifierObject relatedResourceIdentifier) - { - ResourceContext relatedResourceContext = ResourceContextProvider.GetResourceContext(relatedResourceIdentifier.Type); - - if (relatedResourceContext == null) - { - throw new InvalidOperationException($"Included type '{relatedResourceIdentifier.Type}' is not a registered JSON:API resource."); - } - - IIdentifiable relatedInstance = ResourceFactory.CreateInstance(relatedResourceContext.ResourceType); - relatedInstance.StringId = relatedResourceIdentifier.Id; - - ResourceObject includedResource = GetLinkedResource(relatedResourceIdentifier); - - if (includedResource != null) - { - SetAttributes(relatedInstance, includedResource.Attributes, relatedResourceContext.Attributes); - SetRelationships(relatedInstance, includedResource.Relationships, relatedResourceContext.Relationships); - } - - return relatedInstance; - } - - private ResourceObject GetLinkedResource(ResourceIdentifierObject relatedResourceIdentifier) - { - try - { - return Document.Included.SingleOrDefault(resourceObject => - resourceObject.Type == relatedResourceIdentifier.Type && resourceObject.Id == relatedResourceIdentifier.Id); - } - catch (InvalidOperationException exception) - { - throw new InvalidOperationException( - "A compound document MUST NOT include more than one resource object for each type and ID pair." + - $"The duplicate pair was '{relatedResourceIdentifier.Type}, {relatedResourceIdentifier.Id}'", exception); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs b/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs deleted file mode 100644 index 3359cafae6..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Client/Internal/SingleResponse.cs +++ /dev/null @@ -1,18 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.Resources; - -namespace JsonApiDotNetCore.Serialization.Client.Internal -{ - /// - /// Represents a deserialized document with "single data". - /// - /// - /// Type of the resource in the primary data. - /// - [PublicAPI] - public sealed class SingleResponse : DeserializedResponseBase - where TResource : class, IIdentifiable - { - public TResource Data { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs b/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs index 88648a70d2..bc1a7f3e49 100644 --- a/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs +++ b/src/JsonApiDotNetCore/Serialization/ETagGenerator.cs @@ -18,7 +18,7 @@ public ETagGenerator(IFingerprintGenerator fingerprintGenerator) public EntityTagHeaderValue Generate(string requestUrl, string responseBody) { string fingerprint = _fingerprintGenerator.Generate(ArrayFactory.Create(requestUrl, responseBody)); - string eTagValue = "\"" + fingerprint + "\""; + string eTagValue = $"\"{fingerprint}\""; return EntityTagHeaderValue.Parse(eTagValue); } diff --git a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs index d4ace8450d..e19ff666d3 100644 --- a/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs +++ b/src/JsonApiDotNetCore/Serialization/FieldsToSerialize.cs @@ -16,20 +16,20 @@ namespace JsonApiDotNetCore.Serialization [PublicAPI] public class FieldsToSerialize : IFieldsToSerialize { - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly IJsonApiRequest _request; private readonly SparseFieldSetCache _sparseFieldSetCache; /// public bool ShouldSerialize => _request.Kind != EndpointKind.Relationship; - public FieldsToSerialize(IResourceContextProvider resourceContextProvider, IEnumerable constraintProviders, + public FieldsToSerialize(IResourceGraph resourceGraph, IEnumerable constraintProviders, IResourceDefinitionAccessor resourceDefinitionAccessor, IJsonApiRequest request) { - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(request, nameof(request)); - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _request = request; _sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); } @@ -44,7 +44,7 @@ public IReadOnlyCollection GetAttributes(Type resourceType) return Array.Empty(); } - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); IImmutableSet fieldSet = _sparseFieldSetCache.GetSparseFieldSetForSerializer(resourceContext); return SortAttributesInDeclarationOrder(fieldSet, resourceContext).ToArray(); @@ -76,7 +76,7 @@ public IReadOnlyCollection GetRelationships(Type resource return Array.Empty(); } - ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + ResourceContext resourceContext = _resourceGraph.GetResourceContext(resourceType); return resourceContext.Relationships; } diff --git a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs index 3c363cbd4b..ea392575b9 100644 --- a/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/IJsonApiDeserializer.cs @@ -8,8 +8,7 @@ namespace JsonApiDotNetCore.Serialization public interface IJsonApiDeserializer { /// - /// Deserializes JSON into a or and constructs resources from - /// . + /// Deserializes JSON into a and constructs resources from the 'data' element. /// /// /// The JSON to be deserialized. diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs index d9e861d4a2..af634f9876 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs @@ -27,20 +27,19 @@ public class JsonApiReader : IJsonApiReader { private readonly IJsonApiDeserializer _deserializer; private readonly IJsonApiRequest _request; - private readonly IResourceContextProvider _resourceContextProvider; + private readonly IResourceGraph _resourceGraph; private readonly TraceLogWriter _traceWriter; - public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, IResourceContextProvider resourceContextProvider, - ILoggerFactory loggerFactory) + public JsonApiReader(IJsonApiDeserializer deserializer, IJsonApiRequest request, IResourceGraph resourceGraph, ILoggerFactory loggerFactory) { ArgumentGuard.NotNull(deserializer, nameof(deserializer)); ArgumentGuard.NotNull(request, nameof(request)); - ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(loggerFactory, nameof(loggerFactory)); _deserializer = deserializer; _request = request; - _resourceContextProvider = resourceContextProvider; + _resourceGraph = resourceGraph; _traceWriter = new TraceLogWriter(loggerFactory); } @@ -107,8 +106,9 @@ private InvalidRequestBodyException ToInvalidRequestBodyException(JsonApiSeriali if (exception.AtomicOperationIndex != null) { - foreach (Error error in requestException.Errors) + foreach (ErrorObject error in requestException.Errors) { + error.Source ??= new ErrorSource(); error.Source.Pointer = $"/atomic:operations[{exception.AtomicOperationIndex}]"; } } @@ -149,7 +149,7 @@ private static void AssertHasRequestBody(object model, string body) { if (model == null && string.IsNullOrWhiteSpace(body)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Missing request body." }); @@ -171,8 +171,8 @@ private void ValidateIncomingResourceType(object model, HttpRequest httpRequest) { if (!endpointResourceType.IsAssignableFrom(bodyResourceType)) { - ResourceContext resourceFromEndpoint = _resourceContextProvider.GetResourceContext(endpointResourceType); - ResourceContext resourceFromBody = _resourceContextProvider.GetResourceContext(bodyResourceType); + ResourceContext resourceFromEndpoint = _resourceGraph.GetResourceContext(endpointResourceType); + ResourceContext resourceFromBody = _resourceGraph.GetResourceContext(bodyResourceType); throw new ResourceTypeMismatchException(new HttpMethod(httpRequest.Method), httpRequest.Path, resourceFromEndpoint, resourceFromBody); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs index 08c81da0f5..5ca93865c7 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Text; @@ -64,10 +65,10 @@ public async Task WriteAsync(OutputFormatterWriteContext context) catch (Exception exception) #pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException { - ErrorDocument errorDocument = _exceptionHandler.HandleException(exception); - responseContent = _serializer.Serialize(errorDocument); + Document document = _exceptionHandler.HandleException(exception); + responseContent = _serializer.Serialize(document); - response.StatusCode = (int)errorDocument.GetErrorStatusCode(); + response.StatusCode = (int)document.GetErrorStatusCode(); } bool hasMatchingETag = SetETagResponseHeader(request, response, responseContent); @@ -129,14 +130,20 @@ private bool IsSuccessStatusCode(HttpStatusCode statusCode) private static object WrapErrors(object contextObject) { - if (contextObject is IEnumerable errors) + if (contextObject is IEnumerable errors) { - return new ErrorDocument(errors); + return new Document + { + Errors = errors.ToList() + }; } - if (contextObject is Error error) + if (contextObject is ErrorObject error) { - return new ErrorDocument(error); + return new Document + { + Errors = error.AsList() + }; } return contextObject; diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs new file mode 100644 index 0000000000..4f0758fff0 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/ResourceObjectConverter.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to/from JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class ResourceObjectConverter : JsonObjectConverter + { + private static readonly JsonEncodedText TypeText = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText IdText = JsonEncodedText.Encode("id"); + private static readonly JsonEncodedText LidText = JsonEncodedText.Encode("lid"); + private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta"); + private static readonly JsonEncodedText AttributesText = JsonEncodedText.Encode("attributes"); + private static readonly JsonEncodedText RelationshipsText = JsonEncodedText.Encode("relationships"); + private static readonly JsonEncodedText LinksText = JsonEncodedText.Encode("links"); + + private readonly IResourceGraph _resourceGraph; + + public ResourceObjectConverter(IResourceGraph resourceGraph) + { + _resourceGraph = resourceGraph; + } + + /// + /// Resolves the resource type and attributes against the resource graph. Because attribute values in are typed as + /// , we must lookup and supply the target type to the serializer. + /// + public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Inside a JsonConverter there is no way to know where in the JSON object tree we are. And the serializer is unable to provide + // the correct position either. So we avoid an exception on missing/invalid 'type' element and postpone producing an error response + // to the post-processing phase. + + var resourceObject = new ResourceObject + { + // The 'attributes' element may occur before 'type', but we need to know the resource type before we can deserialize attributes + // into their corresponding CLR types. + Type = TryPeekType(ref reader) + }; + + ResourceContext resourceContext = resourceObject.Type != null ? _resourceGraph.TryGetResourceContext(resourceObject.Type) : null; + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndObject: + { + return resourceObject; + } + case JsonTokenType.PropertyName: + { + string propertyName = reader.GetString(); + reader.Read(); + + switch (propertyName) + { + case "id": + { + if (reader.TokenType != JsonTokenType.String) + { + // Newtonsoft.Json used to auto-convert number to strings, while System.Text.Json does not. This is so likely + // to hit users during upgrade that we special-case for this and produce a helpful error message. + var jsonElement = ReadSubTree(ref reader, options); + throw new JsonException($"Failed to convert ID '{jsonElement}' of type '{jsonElement.ValueKind}' to type 'String'."); + } + + resourceObject.Id = reader.GetString(); + break; + } + case "lid": + { + resourceObject.Lid = reader.GetString(); + break; + } + case "attributes": + { + if (resourceContext != null) + { + resourceObject.Attributes = ReadAttributes(ref reader, options, resourceContext); + } + else + { + reader.Skip(); + } + + break; + } + case "relationships": + { + resourceObject.Relationships = ReadSubTree>(ref reader, options); + break; + } + case "links": + { + resourceObject.Links = ReadSubTree(ref reader, options); + break; + } + case "meta": + { + resourceObject.Meta = ReadSubTree>(ref reader, options); + break; + } + default: + { + reader.Skip(); + break; + } + } + + break; + } + } + } + + throw GetEndOfStreamError(); + } + + private static string TryPeekType(ref Utf8JsonReader reader) + { + // https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-5-0#an-alternative-way-to-do-polymorphic-deserialization + Utf8JsonReader readerClone = reader; + + while (readerClone.Read()) + { + if (readerClone.TokenType == JsonTokenType.PropertyName) + { + string propertyName = readerClone.GetString(); + readerClone.Read(); + + switch (propertyName) + { + case "type": + { + return readerClone.GetString(); + } + default: + { + readerClone.Skip(); + break; + } + } + } + } + + return null; + } + + private static IDictionary ReadAttributes(ref Utf8JsonReader reader, JsonSerializerOptions options, ResourceContext resourceContext) + { + var attributes = new Dictionary(); + + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.EndObject: + { + return attributes; + } + case JsonTokenType.PropertyName: + { + string attributeName = reader.GetString(); + reader.Read(); + + AttrAttribute attribute = resourceContext.TryGetAttributeByPublicName(attributeName); + PropertyInfo property = attribute?.Property; + + if (property != null) + { + object attributeValue; + + if (property.Name == nameof(Identifiable.Id)) + { + attributeValue = JsonInvalidAttributeInfo.Id; + } + else + { + try + { + attributeValue = JsonSerializer.Deserialize(ref reader, property.PropertyType, options); + } + catch (JsonException) + { + // Inside a JsonConverter there is no way to know where in the JSON object tree we are. And the serializer + // is unable to provide the correct position either. So we avoid an exception and postpone producing an error + // response to the post-processing phase, by setting a sentinel value. + var jsonElement = ReadSubTree(ref reader, options); + + attributeValue = new JsonInvalidAttributeInfo(attributeName, property.PropertyType, jsonElement.ToString(), + jsonElement.ValueKind); + } + } + + attributes.Add(attributeName!, attributeValue); + } + else + { + reader.Skip(); + } + + break; + } + } + } + + throw GetEndOfStreamError(); + } + + /// + /// Ensures that attribute values are not wrapped in s. + /// + public override void Write(Utf8JsonWriter writer, ResourceObject value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + writer.WriteString(TypeText, value.Type); + + if (value.Id != null) + { + writer.WriteString(IdText, value.Id); + } + + if (value.Lid != null) + { + writer.WriteString(LidText, value.Lid); + } + + if (!value.Attributes.IsNullOrEmpty()) + { + writer.WritePropertyName(AttributesText); + WriteSubTree(writer, value.Attributes, options); + } + + if (!value.Relationships.IsNullOrEmpty()) + { + writer.WritePropertyName(RelationshipsText); + WriteSubTree(writer, value.Relationships, options); + } + + if (value.Links != null && value.Links.HasValue()) + { + writer.WritePropertyName(LinksText); + WriteSubTree(writer, value.Links, options); + } + + if (!value.Meta.IsNullOrEmpty()) + { + writer.WritePropertyName(MetaText); + WriteSubTree(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs new file mode 100644 index 0000000000..0ca65c237e --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to/from JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class SingleOrManyDataConverterFactory : JsonConverterFactory + { + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(SingleOrManyData<>); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type objectType = typeToConvert.GetGenericArguments()[0]; + Type converterType = typeof(SingleOrManyDataConverter<>).MakeGenericType(objectType); + + return (JsonConverter)Activator.CreateInstance(converterType, BindingFlags.Instance | BindingFlags.Public, null, null, null); + } + + private sealed class SingleOrManyDataConverter : JsonObjectConverter> + where T : class, IResourceIdentity + { + public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions serializerOptions) + { + var objects = new List(); + bool isManyData = false; + bool hasCompletedToMany = false; + + do + { + switch (reader.TokenType) + { + case JsonTokenType.EndArray: + { + hasCompletedToMany = true; + break; + } + case JsonTokenType.StartObject: + { + var resourceObject = ReadSubTree(ref reader, serializerOptions); + objects.Add(resourceObject); + break; + } + case JsonTokenType.StartArray: + { + isManyData = true; + break; + } + } + } + while (isManyData && !hasCompletedToMany && reader.Read()); + + object data = isManyData ? objects : objects.FirstOrDefault(); + return new SingleOrManyData(data); + } + + public override void Write(Utf8JsonWriter writer, SingleOrManyData value, JsonSerializerOptions options) + { + WriteSubTree(writer, value.Value, options); + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs new file mode 100644 index 0000000000..65d8f36d12 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyDocumentConverter.cs @@ -0,0 +1,86 @@ +using System; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class WriteOnlyDocumentConverter : JsonObjectConverter + { + private static readonly JsonEncodedText JsonApiText = JsonEncodedText.Encode("jsonapi"); + private static readonly JsonEncodedText LinksText = JsonEncodedText.Encode("links"); + private static readonly JsonEncodedText DataText = JsonEncodedText.Encode("data"); + private static readonly JsonEncodedText AtomicOperationsText = JsonEncodedText.Encode("atomic:operations"); + private static readonly JsonEncodedText AtomicResultsText = JsonEncodedText.Encode("atomic:results"); + private static readonly JsonEncodedText ErrorsText = JsonEncodedText.Encode("errors"); + private static readonly JsonEncodedText IncludedText = JsonEncodedText.Encode("included"); + private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta"); + + public override Document Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("This converter cannot be used for reading JSON."); + } + + /// + /// Conditionally writes "data": null or omits it, depending on . + /// + public override void Write(Utf8JsonWriter writer, Document value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value.JsonApi != null) + { + writer.WritePropertyName(JsonApiText); + WriteSubTree(writer, value.JsonApi, options); + } + + if (value.Links != null && value.Links.HasValue()) + { + writer.WritePropertyName(LinksText); + WriteSubTree(writer, value.Links, options); + } + + if (value.Data.IsAssigned) + { + writer.WritePropertyName(DataText); + WriteSubTree(writer, value.Data, options); + } + + if (!value.Operations.IsNullOrEmpty()) + { + writer.WritePropertyName(AtomicOperationsText); + WriteSubTree(writer, value.Operations, options); + } + + if (!value.Results.IsNullOrEmpty()) + { + writer.WritePropertyName(AtomicResultsText); + WriteSubTree(writer, value.Results, options); + } + + if (!value.Errors.IsNullOrEmpty()) + { + writer.WritePropertyName(ErrorsText); + WriteSubTree(writer, value.Errors, options); + } + + if (value.Included != null) + { + writer.WritePropertyName(IncludedText); + WriteSubTree(writer, value.Included, options); + } + + if (!value.Meta.IsNullOrEmpty()) + { + writer.WritePropertyName(MetaText); + WriteSubTree(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs new file mode 100644 index 0000000000..d80fcee5bd --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/WriteOnlyRelationshipObjectConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Text.Json; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization.JsonConverters +{ + /// + /// Converts to JSON. + /// + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class WriteOnlyRelationshipObjectConverter : JsonObjectConverter + { + private static readonly JsonEncodedText DataText = JsonEncodedText.Encode("data"); + private static readonly JsonEncodedText LinksText = JsonEncodedText.Encode("links"); + private static readonly JsonEncodedText MetaText = JsonEncodedText.Encode("meta"); + + public override RelationshipObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotSupportedException("This converter cannot be used for reading JSON."); + } + + /// + /// Conditionally writes "data": null or omits it, depending on . + /// + public override void Write(Utf8JsonWriter writer, RelationshipObject value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value.Links != null && value.Links.HasValue()) + { + writer.WritePropertyName(LinksText); + WriteSubTree(writer, value.Links, options); + } + + if (value.Data.IsAssigned) + { + writer.WritePropertyName(DataText); + WriteSubTree(writer, value.Data, options); + } + + if (!value.Meta.IsNullOrEmpty()) + { + writer.WritePropertyName(MetaText); + WriteSubTree(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs b/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs new file mode 100644 index 0000000000..037eaf18af --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonInvalidAttributeInfo.cs @@ -0,0 +1,29 @@ +using System; +using System.Text.Json; + +namespace JsonApiDotNetCore.Serialization +{ + /// + /// A sentinel value that is temporarily stored in the attributes dictionary to postpone producing an error. + /// + internal sealed class JsonInvalidAttributeInfo + { + public static readonly JsonInvalidAttributeInfo Id = new("id", typeof(string), "-", JsonValueKind.Undefined); + + public string AttributeName { get; } + public Type AttributeType { get; } + public string JsonValue { get; } + public JsonValueKind JsonType { get; } + + public JsonInvalidAttributeInfo(string attributeName, Type attributeType, string jsonValue, JsonValueKind jsonType) + { + ArgumentGuard.NotNullNorEmpty(attributeName, nameof(attributeName)); + ArgumentGuard.NotNull(attributeType, nameof(attributeType)); + + AttributeName = attributeName; + AttributeType = attributeType; + JsonValue = jsonValue; + JsonType = jsonType; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs new file mode 100644 index 0000000000..2a365317c4 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonObjectConverter.cs @@ -0,0 +1,35 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JsonApiDotNetCore.Serialization +{ + public abstract class JsonObjectConverter : JsonConverter + { + protected static TValue ReadSubTree(ref Utf8JsonReader reader, JsonSerializerOptions options) + { + if (typeof(TValue) != typeof(object) && options?.GetConverter(typeof(TValue)) is JsonConverter converter) + { + return converter.Read(ref reader, typeof(TValue), options); + } + + return JsonSerializer.Deserialize(ref reader, options); + } + + protected static void WriteSubTree(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options) + { + if (typeof(TValue) != typeof(object) && options?.GetConverter(typeof(TValue)) is JsonConverter converter) + { + converter.Write(writer, value, options); + } + else + { + JsonSerializer.Serialize(writer, value, options); + } + } + + protected static JsonException GetEndOfStreamError() + { + return new("Unexpected end of JSON stream."); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs b/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs deleted file mode 100644 index e08b9c3ce0..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonSerializerExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace JsonApiDotNetCore.Serialization -{ - 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/Serialization/Objects/AtomicOperationCode.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs index f7852773b3..c016299024 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationCode.cs @@ -1,12 +1,11 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// See https://jsonapi.org/ext/atomic/#operation-objects. + /// See "op" in https://jsonapi.org/ext/atomic/#operation-objects. /// - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public enum AtomicOperationCode { Add, diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs index b6b4a134b8..27c3f58c44 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationObject.cs @@ -1,25 +1,33 @@ using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// /// See https://jsonapi.org/ext/atomic/#operation-objects. /// - public sealed class AtomicOperationObject : ExposableData + [PublicAPI] + public sealed class AtomicOperationObject { - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Meta { get; set; } + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public SingleOrManyData Data { get; set; } - [JsonProperty("op")] - [JsonConverter(typeof(StringEnumConverter))] + [JsonPropertyName("op")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public AtomicOperationCode Code { get; set; } - [JsonProperty("ref", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("ref")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public AtomicReference Ref { get; set; } - [JsonProperty("href", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("href")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Href { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs deleted file mode 100644 index b7352ed4c6..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicOperationsDocument.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - /// - /// See https://jsonapi.org/ext/atomic/#document-structure. - /// - public sealed class AtomicOperationsDocument - { - /// - /// See "meta" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Meta { get; set; } - - /// - /// See "jsonapi" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)] - public JsonApiObject JsonApi { get; set; } - - /// - /// See "links" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] - public TopLevelLinks Links { get; set; } - - /// - /// See https://jsonapi.org/ext/atomic/#operation-objects. - /// - [JsonProperty("atomic:operations", NullValueHandling = NullValueHandling.Ignore)] - public IList Operations { get; set; } - - /// - /// See https://jsonapi.org/ext/atomic/#result-objects. - /// - [JsonProperty("atomic:results", NullValueHandling = NullValueHandling.Ignore)] - public IList Results { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs index 5847d33fe9..bff24ad299 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs @@ -1,20 +1,28 @@ -using System.Text; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// See 'ref' in https://jsonapi.org/ext/atomic/#operation-objects. + /// See "ref" in https://jsonapi.org/ext/atomic/#operation-objects. /// - public sealed class AtomicReference : ResourceIdentifierObject + [PublicAPI] + public sealed class AtomicReference : IResourceIdentity { - [JsonProperty("relationship", NullValueHandling = NullValueHandling.Ignore)] - public string Relationship { get; set; } + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Type { get; set; } + + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } - protected override void WriteMembers(StringBuilder builder) - { - base.WriteMembers(builder); - WriteMember(builder, "relationship", Relationship); - } + [JsonPropertyName("lid")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Lid { get; set; } + + [JsonPropertyName("relationship")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Relationship { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs index 44b5f691d7..14f67a5247 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicResultObject.cs @@ -1,14 +1,21 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// /// See https://jsonapi.org/ext/atomic/#result-objects. /// - public sealed class AtomicResultObject : ExposableData + [PublicAPI] + public sealed class AtomicResultObject { - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary Meta { get; set; } + [JsonPropertyName("data")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public SingleOrManyData Data { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index 56c79f2b86..ae3a09b9b1 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,35 +1,64 @@ +using System; using System.Collections.Generic; -using Newtonsoft.Json; +using System.Linq; +using System.Net; +using System.Text.Json.Serialization; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// https://jsonapi.org/format/#document-structure + /// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure. /// - public sealed class Document : ExposableData + public sealed class Document { - /// - /// see "meta" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Meta { get; set; } - - /// - /// see "jsonapi" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("jsonapi", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("jsonapi")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public JsonApiObject JsonApi { get; set; } - /// - /// see "links" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public TopLevelLinks Links { get; set; } - /// - /// see "included" in https://jsonapi.org/format/#document-top-level - /// - [JsonProperty("included", NullValueHandling = NullValueHandling.Ignore, Order = 1)] + [JsonPropertyName("data")] + // JsonIgnoreCondition is determined at runtime by WriteOnlyDocumentConverter. + public SingleOrManyData Data { get; set; } + + [JsonPropertyName("atomic:operations")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Operations { get; set; } + + [JsonPropertyName("atomic:results")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Results { get; set; } + + [JsonPropertyName("errors")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Errors { get; set; } + + [JsonPropertyName("included")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IList Included { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } + + internal HttpStatusCode GetErrorStatusCode() + { + if (Errors.IsNullOrEmpty()) + { + throw new InvalidOperationException("No errors found."); + } + + int[] statusCodes = Errors.Select(error => (int)error.StatusCode).Distinct().ToArray(); + + if (statusCodes.Length == 1) + { + return (HttpStatusCode)statusCodes[0]; + } + + int statusCode = int.Parse($"{statusCodes.Max().ToString()[0]}00"); + return (HttpStatusCode)statusCode; + } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs b/src/JsonApiDotNetCore/Serialization/Objects/Error.cs deleted file mode 100644 index f3afb594e8..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/Error.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using JetBrains.Annotations; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - /// - /// Provides additional information about a problem encountered while performing an operation. Error objects MUST be returned as an array keyed by errors - /// in the top level of a JSON:API document. - /// - [PublicAPI] - public sealed class Error - { - /// - /// A unique identifier for this particular occurrence of the problem. - /// - [JsonProperty] - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// A link that leads to further details about this particular occurrence of the problem. - /// - [JsonProperty] - public ErrorLinks Links { get; set; } = new(); - - /// - /// The HTTP status code applicable to this problem. - /// - [JsonIgnore] - public HttpStatusCode StatusCode { get; set; } - - [JsonProperty] - public string Status - { - get => StatusCode.ToString("d"); - set => StatusCode = (HttpStatusCode)int.Parse(value); - } - - /// - /// An application-specific error code. - /// - [JsonProperty] - public string Code { get; set; } - - /// - /// A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of - /// localization. - /// - [JsonProperty] - public string Title { get; set; } - - /// - /// A human-readable explanation specific to this occurrence of the problem. Like title, this field's value can be localized. - /// - [JsonProperty] - public string Detail { get; set; } - - /// - /// An object containing references to the source of the error. - /// - [JsonProperty] - public ErrorSource Source { get; set; } = new(); - - /// - /// An object containing non-standard meta-information (key/value pairs) about the error. - /// - [JsonProperty] - public ErrorMeta Meta { get; set; } = new(); - - public Error(HttpStatusCode statusCode) - { - StatusCode = statusCode; - } - - public bool ShouldSerializeLinks() - { - return Links?.About != null; - } - - public bool ShouldSerializeSource() - { - return Source != null && (Source.Pointer != null || Source.Parameter != null); - } - - public bool ShouldSerializeMeta() - { - return Meta != null && Meta.Data.Any(); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs deleted file mode 100644 index 971fdecce3..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorDocument.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - [PublicAPI] - public sealed class ErrorDocument - { - public IReadOnlyList Errors { get; } - - public ErrorDocument() - : this(Array.Empty()) - { - } - - public ErrorDocument(Error error) - : this(error.AsEnumerable()) - { - } - - public ErrorDocument(IEnumerable errors) - { - ArgumentGuard.NotNull(errors, nameof(errors)); - - Errors = errors.ToList(); - } - - public HttpStatusCode GetErrorStatusCode() - { - int[] statusCodes = Errors.Select(error => (int)error.StatusCode).Distinct().ToArray(); - - if (statusCodes.Length == 1) - { - return (HttpStatusCode)statusCodes[0]; - } - - int statusCode = int.Parse(statusCodes.Max().ToString()[0] + "00"); - return (HttpStatusCode)statusCode; - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs index 16be6392e1..3b03f68cda 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorLinks.cs @@ -1,13 +1,20 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See "links" in https://jsonapi.org/format/1.1/#error-objects. + /// + [PublicAPI] public sealed class ErrorLinks { - /// - /// A URL that leads to further details about this particular occurrence of the problem. - /// - [JsonProperty] + [JsonPropertyName("about")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string About { get; set; } + + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Type { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs deleted file mode 100644 index 1589089719..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorMeta.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Annotations; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - /// - /// A meta object containing non-standard meta-information about the error. - /// - [PublicAPI] - public sealed class ErrorMeta - { - [JsonExtensionData] - public IDictionary Data { get; } = new Dictionary(); - - public void IncludeExceptionStackTrace(Exception exception) - { - if (exception == null) - { - Data.Remove("StackTrace"); - } - else - { - Data["StackTrace"] = exception.ToString().Split("\n", int.MaxValue, StringSplitOptions.RemoveEmptyEntries); - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs new file mode 100644 index 0000000000..a5ac6be1a8 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorObject.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// See https://jsonapi.org/format/1.1/#error-objects. + /// + [PublicAPI] + public sealed class ErrorObject + { + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } = Guid.NewGuid().ToString(); + + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ErrorLinks Links { get; set; } + + [JsonIgnore] + public HttpStatusCode StatusCode { get; set; } + + [JsonPropertyName("status")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Status + { + get => StatusCode.ToString("d"); + set => StatusCode = (HttpStatusCode)int.Parse(value); + } + + [JsonPropertyName("code")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Code { get; set; } + + [JsonPropertyName("title")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Title { get; set; } + + [JsonPropertyName("detail")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Detail { get; set; } + + [JsonPropertyName("source")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public ErrorSource Source { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } + + public ErrorObject(HttpStatusCode statusCode) + { + StatusCode = statusCode; + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs index 88cdc1812d..ebd8ee49bd 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ErrorSource.cs @@ -1,20 +1,24 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See "source" in https://jsonapi.org/format/1.1/#error-objects. + /// + [PublicAPI] public sealed class ErrorSource { - /// - /// Optional. A JSON Pointer [RFC6901] to the associated resource in the request document [e.g. "/data" for a primary data object, or - /// "/data/attributes/title" for a specific attribute]. - /// - [JsonProperty] + [JsonPropertyName("pointer")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Pointer { get; set; } - /// - /// Optional. A string indicating which URI query parameter caused the error. - /// - [JsonProperty] + [JsonPropertyName("parameter")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Parameter { get; set; } + + [JsonPropertyName("header")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Header { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs b/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs deleted file mode 100644 index 27ef8d0690..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/ExposableData.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - [PublicAPI] - public abstract class ExposableData - where TResource : class - { - private bool IsEmpty => !HasManyData && SingleData == null; - - private bool HasManyData => IsManyData && ManyData.Any(); - - /// - /// 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). - /// - internal bool IsPopulated { get; private set; } - - internal bool HasResource => IsPopulated && !IsEmpty; - - /// - /// See "primary data" in https://jsonapi.org/format/#document-top-level. - /// - [JsonProperty("data")] - public object Data - { - get => GetPrimaryData(); - set => SetPrimaryData(value); - } - - /// - /// Internally used for "single" primary data. - /// - [JsonIgnore] - public TResource SingleData { get; private set; } - - /// - /// Internally used for "many" primary data. - /// - [JsonIgnore] - public IList ManyData { get; private set; } - - /// - /// Indicates if the document's primary data is "single" or "many". - /// - [JsonIgnore] - public bool IsManyData { get; private set; } - - /// - /// See https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm. - /// - /// - /// Moving this method to the derived class where it is needed only in the case of would make more sense, but Newtonsoft - /// does not support this. - /// - public bool ShouldSerializeData() - { - if (GetType() == typeof(RelationshipEntry)) - { - return IsPopulated; - } - - return true; - } - - /// - /// Gets the "single" or "many" data depending on which one was assigned in this document. - /// - protected object GetPrimaryData() - { - if (IsManyData) - { - return ManyData; - } - - return SingleData; - } - - /// - /// Sets the primary data depending on if it is "single" or "many" data. - /// - protected void SetPrimaryData(object value) - { - IsPopulated = true; - - if (value is JObject jObject) - { - SingleData = jObject.ToObject(); - } - else if (value is TResource ro) - { - SingleData = ro; - } - else if (value != null) - { - IsManyData = true; - - if (value is JArray jArray) - { - ManyData = jArray.ToObject>(); - } - else - { - ManyData = (List)value; - } - } - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs b/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs new file mode 100644 index 0000000000..ff936f4d46 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.Serialization.Objects +{ + public interface IResourceIdentity + { + public string Type { get; } + public string Id { get; } + public string Lid { get; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs index 66682fb6a2..11b214b434 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/JsonApiObject.cs @@ -1,26 +1,29 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// https://jsonapi.org/format/1.1/#document-jsonapi-object. + /// See https://jsonapi.org/format/1.1/#document-jsonapi-object. /// + [PublicAPI] public sealed class JsonApiObject { - [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("version")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Version { get; set; } - [JsonProperty("ext", NullValueHandling = NullValueHandling.Ignore)] - public ICollection Ext { get; set; } + [JsonPropertyName("ext")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Ext { get; set; } - [JsonProperty("profile", NullValueHandling = NullValueHandling.Ignore)] - public ICollection Profile { get; set; } + [JsonPropertyName("profile")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IList Profile { get; set; } - /// - /// see "meta" in https://jsonapi.org/format/1.1/#document-meta - /// - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs deleted file mode 100644 index 8ebbbf1b16..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipEntry.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace JsonApiDotNetCore.Serialization.Objects -{ - public sealed class RelationshipEntry : ExposableData - { - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] - public RelationshipLinks Links { get; set; } - - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Meta { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs index e0ee4c680f..b66f33daa8 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipLinks.cs @@ -1,19 +1,20 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See "links" in https://jsonapi.org/format/1.1/#document-resource-object-relationships. + /// + [PublicAPI] public sealed class RelationshipLinks { - /// - /// See "links" bulletin at https://jsonapi.org/format/#document-resource-object-relationships. - /// - [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("self")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Self { get; set; } - /// - /// See https://jsonapi.org/format/#document-resource-object-related-resource-links. - /// - [JsonProperty("related", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("related")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Related { get; set; } internal bool HasValue() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs new file mode 100644 index 0000000000..fb4296d70d --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/RelationshipObject.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// See https://jsonapi.org/format/1.1/#document-resource-object-relationships. + /// + [PublicAPI] + public sealed class RelationshipObject + { + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RelationshipLinks Links { get; set; } + + [JsonPropertyName("data")] + // JsonIgnoreCondition is determined at runtime by WriteOnlyRelationshipObjectConverter. + public SingleOrManyData Data { get; set; } + + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs index 672255d96e..de4104d28a 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs @@ -1,50 +1,29 @@ -using System.Text; -using Newtonsoft.Json; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { - public class ResourceIdentifierObject + /// + /// See https://jsonapi.org/format/1.1/#document-resource-identifier-objects. + /// + [PublicAPI] + public sealed class ResourceIdentifierObject : IResourceIdentity { - [JsonProperty("type", Order = -4)] + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] public string Type { get; set; } - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore, Order = -3)] + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Id { get; set; } - [JsonProperty("lid", NullValueHandling = NullValueHandling.Ignore, Order = -2)] + [JsonPropertyName("lid")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Lid { get; set; } - public override string ToString() - { - var builder = new StringBuilder(); - - WriteMembers(builder); - builder.Insert(0, GetType().Name + ": "); - - return builder.ToString(); - } - - protected virtual void WriteMembers(StringBuilder builder) - { - WriteMember(builder, "type", Type); - WriteMember(builder, "id", Id); - WriteMember(builder, "lid", Lid); - } - - protected static void WriteMember(StringBuilder builder, string memberName, string memberValue) - { - if (memberValue != null) - { - if (builder.Length > 0) - { - builder.Append(", "); - } - - builder.Append(memberName); - builder.Append("=\""); - builder.Append(memberValue); - builder.Append('"'); - } - } + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs index 3afbd3ffbd..7ab1f6861e 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceLinks.cs @@ -1,13 +1,16 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { + /// + /// See https://jsonapi.org/format/1.1/#document-resource-object-links. + /// + [PublicAPI] public sealed class ResourceLinks { - /// - /// See https://jsonapi.org/format/#document-resource-object-links. - /// - [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("self")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Self { get; set; } internal bool HasValue() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs index c3ae877787..f418a63ed1 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs @@ -1,20 +1,41 @@ using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { - public sealed class ResourceObject : ResourceIdentifierObject + /// + /// See https://jsonapi.org/format/1.1/#document-resource-objects. + /// + [PublicAPI] + public sealed class ResourceObject : IResourceIdentity { - [JsonProperty("attributes", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("type")] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string Type { get; set; } + + [JsonPropertyName("id")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Id { get; set; } + + [JsonPropertyName("lid")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string Lid { get; set; } + + [JsonPropertyName("attributes")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary Attributes { get; set; } - [JsonProperty("relationships", NullValueHandling = NullValueHandling.Ignore)] - public IDictionary Relationships { get; set; } + [JsonPropertyName("relationships")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IDictionary Relationships { get; set; } - [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("links")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ResourceLinks Links { get; set; } - [JsonProperty("meta", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("meta")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary Meta { get; set; } } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs new file mode 100644 index 0000000000..c2a6c23876 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; +using JsonApiDotNetCore.Serialization.JsonConverters; + +namespace JsonApiDotNetCore.Serialization.Objects +{ + /// + /// Represents the value of the "data" element, which is either null, a single object or an array of objects. Add + /// to to properly roundtrip. + /// + [PublicAPI] + public readonly struct SingleOrManyData + where T : class, IResourceIdentity + { + // ReSharper disable once MergeConditionalExpression + // Justification: ReSharper reporting this is a bug, which is fixed in v2021.2.1. This condition cannot be merged. + public object Value => ManyValue != null ? ManyValue : SingleValue; + + [JsonIgnore] + public bool IsAssigned { get; } + + [JsonIgnore] + public T SingleValue { get; } + + [JsonIgnore] + public IList ManyValue { get; } + + public SingleOrManyData(object value) + { + IsAssigned = true; + + if (value is IEnumerable manyData) + { + ManyValue = manyData.ToList(); + SingleValue = null; + } + else + { + ManyValue = null; + SingleValue = (T)value; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs index 6970eecb8a..0817e56d8a 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/TopLevelLinks.cs @@ -1,73 +1,46 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects { /// - /// See links section in https://jsonapi.org/format/#document-top-level. + /// See "links" in https://jsonapi.org/format/1.1/#document-top-level. /// + [PublicAPI] public sealed class TopLevelLinks { - [JsonProperty("self")] + [JsonPropertyName("self")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Self { get; set; } - [JsonProperty("related")] + [JsonPropertyName("related")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Related { get; set; } - [JsonProperty("describedby")] + [JsonPropertyName("describedby")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string DescribedBy { get; set; } - [JsonProperty("first")] + [JsonPropertyName("first")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string First { get; set; } - [JsonProperty("last")] + [JsonPropertyName("last")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Last { get; set; } - [JsonProperty("prev")] + [JsonPropertyName("prev")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Prev { get; set; } - [JsonProperty("next")] + [JsonPropertyName("next")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Next { get; set; } - // http://www.newtonsoft.com/json/help/html/ConditionalProperties.htm - public bool ShouldSerializeSelf() - { - return !string.IsNullOrEmpty(Self); - } - - public bool ShouldSerializeRelated() - { - return !string.IsNullOrEmpty(Related); - } - - public bool ShouldSerializeDescribedBy() - { - return !string.IsNullOrEmpty(DescribedBy); - } - - public bool ShouldSerializeFirst() - { - return !string.IsNullOrEmpty(First); - } - - public bool ShouldSerializeLast() - { - return !string.IsNullOrEmpty(Last); - } - - public bool ShouldSerializePrev() - { - return !string.IsNullOrEmpty(Prev); - } - - public bool ShouldSerializeNext() - { - return !string.IsNullOrEmpty(Next); - } - internal bool HasValue() { - return ShouldSerializeSelf() || ShouldSerializeRelated() || ShouldSerializeDescribedBy() || ShouldSerializeFirst() || ShouldSerializeLast() || - ShouldSerializePrev() || ShouldSerializeNext(); + return !string.IsNullOrEmpty(Self) || !string.IsNullOrEmpty(Related) || !string.IsNullOrEmpty(DescribedBy) || !string.IsNullOrEmpty(First) || + !string.IsNullOrEmpty(Last) || !string.IsNullOrEmpty(Prev) || !string.IsNullOrEmpty(Next); } } } diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs index 8220936b20..3b466a7087 100644 --- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs @@ -6,14 +6,12 @@ using Humanizer; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Diagnostics; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Resources.Internal; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; -using Newtonsoft.Json.Linq; namespace JsonApiDotNetCore.Serialization { @@ -29,9 +27,9 @@ public class RequestDeserializer : BaseDeserializer, IJsonApiDeserializer private readonly IJsonApiOptions _options; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; - public RequestDeserializer(IResourceContextProvider resourceContextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, + public RequestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor, IJsonApiRequest request, IJsonApiOptions options, IResourceDefinitionAccessor resourceDefinitionAccessor) - : base(resourceContextProvider, resourceFactory) + : base(resourceGraph, resourceFactory) { ArgumentGuard.NotNull(targetedFields, nameof(targetedFields)); ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor)); @@ -61,7 +59,7 @@ public object Deserialize(string body) return DeserializeOperationsDocument(body); } - object instance = DeserializeBody(body); + object instance = DeserializeData(body, _options.SerializerReadOptions); if (instance is IIdentifiable resource && _request.Kind != EndpointKind.Relationship) { @@ -75,13 +73,7 @@ public object Deserialize(string body) private object DeserializeOperationsDocument(string body) { - AtomicOperationsDocument document; - - using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages)) - { - JToken bodyToken = LoadJToken(body); - document = bodyToken.ToObject(); - } + Document document = DeserializeDocument(body, _options.SerializerReadOptions); if ((document?.Operations).IsNullOrEmpty()) { @@ -216,7 +208,7 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati _request.CopyFrom(request); - IIdentifiable primaryResource = ParseResourceObject(operation.SingleData); + IIdentifiable primaryResource = ParseResourceObject(operation.Data.SingleValue); _resourceDefinitionAccessor.OnDeserialize(primaryResource); @@ -236,33 +228,33 @@ private OperationContainer ParseForCreateOrUpdateResourceOperation(AtomicOperati private ResourceObject GetRequiredSingleDataForResourceOperation(AtomicOperationObject operation) { - if (operation.Data == null) + if (operation.Data.Value == null) { throw new JsonApiSerializationException("The 'data' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } - if (operation.SingleData == null) + if (operation.Data.SingleValue == null) { throw new JsonApiSerializationException("Expected single data element for create/update resource operation.", null, atomicOperationIndex: AtomicOperationIndex); } - return operation.SingleData; + return operation.Data.SingleValue; } [AssertionMethod] - private void AssertElementHasType(ResourceIdentifierObject resourceIdentifierObject, string elementPath) + private void AssertElementHasType(IResourceIdentity resourceIdentity, string elementPath) { - if (resourceIdentifierObject.Type == null) + if (resourceIdentity.Type == null) { throw new JsonApiSerializationException($"The '{elementPath}.type' element is required.", null, atomicOperationIndex: AtomicOperationIndex); } } - private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifierObject, string elementPath, bool isRequired) + private void AssertElementHasIdOrLid(IResourceIdentity resourceIdentity, string elementPath, bool isRequired) { - bool hasNone = resourceIdentifierObject.Id == null && resourceIdentifierObject.Lid == null; - bool hasBoth = resourceIdentifierObject.Id != null && resourceIdentifierObject.Lid != null; + bool hasNone = resourceIdentity.Id == null && resourceIdentity.Lid == null; + bool hasBoth = resourceIdentity.Id != null && resourceIdentity.Lid != null; if (isRequired ? hasNone || hasBoth : hasBoth) { @@ -271,13 +263,13 @@ private void AssertElementHasIdOrLid(ResourceIdentifierObject resourceIdentifier } } - private void AssertCompatibleId(ResourceIdentifierObject resourceIdentifierObject, Type idType) + private void AssertCompatibleId(IResourceIdentity resourceIdentity, Type idType) { - if (resourceIdentifierObject.Id != null) + if (resourceIdentity.Id != null) { try { - RuntimeTypeConverter.ConvertType(resourceIdentifierObject.Id, idType); + RuntimeTypeConverter.ConvertType(resourceIdentity.Id, idType); } catch (FormatException exception) { @@ -286,33 +278,33 @@ private void AssertCompatibleId(ResourceIdentifierObject resourceIdentifierObjec } } - private void AssertSameIdentityInRefData(AtomicOperationObject operation, ResourceIdentifierObject resourceIdentifierObject) + private void AssertSameIdentityInRefData(AtomicOperationObject operation, IResourceIdentity resourceIdentity) { - if (operation.Ref.Id != null && resourceIdentifierObject.Id != null && resourceIdentifierObject.Id != operation.Ref.Id) + if (operation.Ref.Id != null && resourceIdentity.Id != null && resourceIdentity.Id != operation.Ref.Id) { throw new JsonApiSerializationException("Resource ID mismatch between 'ref.id' and 'data.id' element.", - $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Id}'.", + $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentity.Id}'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Lid != null && resourceIdentifierObject.Lid != null && resourceIdentifierObject.Lid != operation.Ref.Lid) + if (operation.Ref.Lid != null && resourceIdentity.Lid != null && resourceIdentity.Lid != operation.Ref.Lid) { throw new JsonApiSerializationException("Resource local ID mismatch between 'ref.lid' and 'data.lid' element.", - $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Lid}'.", + $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentity.Lid}'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Id != null && resourceIdentifierObject.Lid != null) + if (operation.Ref.Id != null && resourceIdentity.Lid != null) { throw new JsonApiSerializationException("Resource identity mismatch between 'ref.id' and 'data.lid' element.", - $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentifierObject.Lid}' in 'data.lid'.", + $"Expected resource with ID '{operation.Ref.Id}' in 'data.id', instead of '{resourceIdentity.Lid}' in 'data.lid'.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.Ref.Lid != null && resourceIdentifierObject.Id != null) + if (operation.Ref.Lid != null && resourceIdentity.Id != null) { throw new JsonApiSerializationException("Resource identity mismatch between 'ref.lid' and 'data.id' element.", - $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentifierObject.Id}' in 'data.id'.", + $"Expected resource with local ID '{operation.Ref.Lid}' in 'data.lid', instead of '{resourceIdentity.Id}' in 'data.id'.", atomicOperationIndex: AtomicOperationIndex); } } @@ -362,7 +354,7 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o $"Relationship '{operation.Ref.Relationship}' must be a to-many relationship.", atomicOperationIndex: AtomicOperationIndex); } - ResourceContext secondaryResourceContext = ResourceContextProvider.GetResourceContext(relationship.RightType); + ResourceContext secondaryResourceContext = ResourceGraph.GetResourceContext(relationship.RightType); var request = new JsonApiRequest { @@ -392,7 +384,7 @@ private OperationContainer ParseForRelationshipOperation(AtomicOperationObject o private RelationshipAttribute GetExistingRelationship(AtomicReference reference, ResourceContext resourceContext) { - RelationshipAttribute relationship = resourceContext.Relationships.FirstOrDefault(attribute => attribute.PublicName == reference.Relationship); + RelationshipAttribute relationship = resourceContext.TryGetRelationshipByPublicName(reference.Relationship); if (relationship == null) { @@ -409,23 +401,23 @@ private void ParseDataForRelationship(RelationshipAttribute relationship, Resour { if (relationship is HasOneAttribute) { - if (operation.ManyData != null) + if (operation.Data.ManyValue != null) { throw new JsonApiSerializationException("Expected single data element for to-one relationship.", $"Expected single data element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); } - if (operation.SingleData != null) + if (operation.Data.SingleValue != null) { - ValidateSingleDataForRelationship(operation.SingleData, secondaryResourceContext, "data"); + ValidateSingleDataForRelationship(operation.Data.SingleValue, secondaryResourceContext, "data"); - IIdentifiable secondaryResource = ParseResourceObject(operation.SingleData); + IIdentifiable secondaryResource = ParseResourceObject(operation.Data.SingleValue); relationship.SetValue(primaryResource, secondaryResource); } } else if (relationship is HasManyAttribute) { - if (operation.ManyData == null) + if (operation.Data.ManyValue == null) { throw new JsonApiSerializationException("Expected data[] element for to-many relationship.", $"Expected data[] element for '{relationship.PublicName}' relationship.", atomicOperationIndex: AtomicOperationIndex); @@ -433,7 +425,7 @@ private void ParseDataForRelationship(RelationshipAttribute relationship, Resour var secondaryResources = new List(); - foreach (ResourceObject resourceObject in operation.ManyData) + foreach (ResourceObject resourceObject in operation.Data.ManyValue) { ValidateSingleDataForRelationship(resourceObject, secondaryResourceContext, "data[]"); @@ -488,7 +480,7 @@ private void AssertResourceIdIsNotTargeted(ITargetedFields targetedFields) /// /// Relationship data for . Is null when is not a . /// - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) + protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null) { bool isCreatingResource = IsCreatingResource(); bool isUpdatingResource = IsUpdatingResource(); diff --git a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs index bc5fd805c0..348e1d7a2d 100644 --- a/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs +++ b/src/JsonApiDotNetCore/Serialization/ResponseSerializer.cs @@ -8,7 +8,6 @@ using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; namespace JsonApiDotNetCore.Serialization { @@ -72,7 +71,7 @@ public string Serialize(object content) return SerializeMany(collectionOfIdentifiable.ToArray()); } - if (content is ErrorDocument errorDocument) + if (content is Document errorDocument) { return SerializeErrorDocument(errorDocument); } @@ -80,12 +79,11 @@ public string Serialize(object content) throw new InvalidOperationException("Data being returned must be errors or resources."); } - private string SerializeErrorDocument(ErrorDocument errorDocument) + private string SerializeErrorDocument(Document document) { - return SerializeObject(errorDocument, _options.SerializerSettings, serializer => - { - serializer.ApplyErrorSettings(); - }); + SetApiVersion(document); + + return SerializeObject(document, _options.SerializerWriteOptions); } /// @@ -105,7 +103,7 @@ internal string SerializeSingle(IIdentifiable resource) IReadOnlyCollection relationships = _fieldsToSerialize.GetRelationships(_primaryResourceType); Document document = Build(resource, attributes, relationships); - ResourceObject resourceObject = document.SingleData; + ResourceObject resourceObject = document.Data.SingleValue; if (resourceObject != null) { @@ -114,10 +112,7 @@ internal string SerializeSingle(IIdentifiable resource) AddTopLevelObjects(document); - return SerializeObject(document, _options.SerializerSettings, serializer => - { - serializer.NullValueHandling = NullValueHandling.Include; - }); + return SerializeObject(document, _options.SerializerWriteOptions); } /// @@ -141,7 +136,7 @@ internal string SerializeMany(IReadOnlyCollection resources) Document document = Build(resources, attributes, relationships); - foreach (ResourceObject resourceObject in document.ManyData) + foreach (ResourceObject resourceObject in document.Data.ManyValue) { ResourceLinks links = _linkBuilder.GetResourceLinks(resourceObject.Type, resourceObject.Id); @@ -155,16 +150,22 @@ internal string SerializeMany(IReadOnlyCollection resources) AddTopLevelObjects(document); - return SerializeObject(document, _options.SerializerSettings, serializer => - { - serializer.NullValueHandling = NullValueHandling.Include; - }); + return SerializeObject(document, _options.SerializerWriteOptions); } /// /// Adds top-level objects that are only added to a document in the case of server-side serialization. /// private void AddTopLevelObjects(Document document) + { + SetApiVersion(document); + + document.Links = _linkBuilder.GetTopLevelLinks(); + document.Meta = _metaBuilder.Build(); + document.Included = _includedBuilder.Build(); + } + + private void SetApiVersion(Document document) { if (_options.IncludeJsonApiVersion) { @@ -173,10 +174,6 @@ private void AddTopLevelObjects(Document document) Version = "1.1" }; } - - document.Links = _linkBuilder.GetTopLevelLinks(); - document.Meta = _metaBuilder.Build(); - document.Included = _includedBuilder.Build(); } } } diff --git a/src/JsonApiDotNetCore/TypeExtensions.cs b/src/JsonApiDotNetCore/TypeExtensions.cs index d25599b821..15713ac5fa 100644 --- a/src/JsonApiDotNetCore/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/TypeExtensions.cs @@ -19,5 +19,30 @@ public static bool IsOrImplementsInterface(this Type source, Type interfaceType) return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType); } + + /// + /// Gets the name of a type, including the names of its generic type parameters. + /// + /// > + /// ]]> + /// + /// + public static string GetFriendlyTypeName(this Type type) + { + ArgumentGuard.NotNull(type, nameof(type)); + + // Based on https://stackoverflow.com/questions/2581642/how-do-i-get-the-type-name-of-a-generic-type-argument. + + if (type.IsGenericType) + { + string genericArguments = type.GetGenericArguments().Select(GetFriendlyTypeName) + .Aggregate((firstType, secondType) => $"{firstType}, {secondType}"); + + return $"{type.Name[..type.Name.IndexOf("`", StringComparison.Ordinal)]}" + $"<{genericArguments}>"; + } + + return type.Name; + } } } diff --git a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs index 8392b92baa..a5a2f9fd77 100644 --- a/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs +++ b/test/DiscoveryTests/ServiceDiscoveryFacadeTests.cs @@ -33,7 +33,6 @@ public ServiceDiscoveryFacadeTests() _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); - _services.AddScoped(_ => new Mock().Object); _services.AddScoped(typeof(IResourceChangeTracker<>), typeof(ResourceChangeTracker<>)); _services.AddScoped(_ => new Mock().Object); _services.AddScoped(_ => new Mock().Object); @@ -57,10 +56,10 @@ public void Can_add_resources_from_assembly_to_graph() // Assert IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - ResourceContext personContext = resourceGraph.GetResourceContext(typeof(Person)); + ResourceContext personContext = resourceGraph.TryGetResourceContext(typeof(Person)); personContext.Should().NotBeNull(); - ResourceContext todoItemContext = resourceGraph.GetResourceContext(typeof(TodoItem)); + ResourceContext todoItemContext = resourceGraph.TryGetResourceContext(typeof(TodoItem)); todoItemContext.Should().NotBeNull(); } @@ -77,7 +76,7 @@ public void Can_add_resource_from_current_assembly_to_graph() // Assert IResourceGraph resourceGraph = _resourceGraphBuilder.Build(); - ResourceContext testContext = resourceGraph.GetResourceContext(typeof(TestResource)); + ResourceContext testContext = resourceGraph.TryGetResourceContext(typeof(TestResource)); testContext.Should().NotBeNull(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs index d1a77728ac..2634ffae2a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/ArchiveTests.cs @@ -44,7 +44,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -52,9 +52,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(broadcast.StringId); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeCloseTo(broadcast.ArchivedAt); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(broadcast.StringId); + responseDocument.Data.SingleValue.Attributes["archivedAt"].As().Should().BeCloseTo(broadcast.ArchivedAt.GetValueOrDefault()); } [Fact] @@ -70,7 +70,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -78,9 +78,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(broadcast.StringId); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(broadcast.StringId); + responseDocument.Data.SingleValue.Attributes["archivedAt"].As().Should().BeNull(); } [Fact] @@ -105,9 +105,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(broadcasts[1].StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(broadcasts[1].StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].As().Should().BeNull(); } [Fact] @@ -132,11 +132,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(broadcasts[0].StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeCloseTo(broadcasts[0].ArchivedAt); - responseDocument.ManyData[1].Id.Should().Be(broadcasts[1].StringId); - responseDocument.ManyData[1].Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(broadcasts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].As().Should().BeCloseTo(broadcasts[0].ArchivedAt.GetValueOrDefault()); + responseDocument.Data.ManyValue[1].Id.Should().Be(broadcasts[1].StringId); + responseDocument.Data.ManyValue[1].Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -161,8 +161,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(station.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(station.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); @@ -192,12 +192,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(station.StringId); + DateTimeOffset archivedAt0 = station.Broadcasts.ElementAt(0).ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(station.StringId); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); - responseDocument.Included[0].Attributes["archivedAt"].Should().BeCloseTo(station.Broadcasts.ElementAt(0).ArchivedAt); + responseDocument.Included[0].Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt0); responseDocument.Included[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); responseDocument.Included[1].Attributes["archivedAt"].Should().BeNull(); } @@ -223,9 +225,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.AppliesTo.StringId); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeCloseTo(comment.AppliesTo.ArchivedAt); + DateTimeOffset archivedAt = comment.AppliesTo.ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.AppliesTo.StringId); + responseDocument.Data.SingleValue.Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt); } [Fact] @@ -250,9 +254,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -277,11 +281,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); - responseDocument.ManyData[0].Attributes["archivedAt"].Should().BeCloseTo(station.Broadcasts.ElementAt(0).ArchivedAt); - responseDocument.ManyData[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); - responseDocument.ManyData[1].Attributes["archivedAt"].Should().BeNull(); + DateTimeOffset archivedAt0 = station.Broadcasts.ElementAt(0).ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); + responseDocument.Data.ManyValue[0].Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt0); + responseDocument.Data.ManyValue[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue[1].Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -307,8 +313,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(network.Stations.ElementAt(0).Broadcasts.ElementAt(1).StringId); @@ -338,12 +344,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); + DateTimeOffset archivedAt0 = network.Stations.ElementAt(0).Broadcasts.ElementAt(0).ArchivedAt.GetValueOrDefault(); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(network.Stations.ElementAt(0).StringId); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(network.Stations.ElementAt(0).Broadcasts.ElementAt(0).StringId); - responseDocument.Included[0].Attributes["archivedAt"].Should().BeCloseTo(network.Stations.ElementAt(0).Broadcasts.ElementAt(0).ArchivedAt); + responseDocument.Included[0].Attributes["archivedAt"].As().Should().BeCloseTo(archivedAt0); responseDocument.Included[1].Id.Should().Be(network.Stations.ElementAt(0).Broadcasts.ElementAt(1).StringId); responseDocument.Included[1].Attributes["archivedAt"].Should().BeNull(); } @@ -370,8 +378,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); } [Fact] @@ -396,9 +404,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); - responseDocument.ManyData[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(station.Broadcasts.ElementAt(0).StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(station.Broadcasts.ElementAt(1).StringId); } [Fact] @@ -428,10 +436,10 @@ public async Task Can_create_unarchived_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["title"].Should().Be(newBroadcast.Title); - responseDocument.SingleData.Attributes["airedAt"].Should().BeCloseTo(newBroadcast.AiredAt); - responseDocument.SingleData.Attributes["archivedAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(newBroadcast.Title); + responseDocument.Data.SingleValue.Attributes["airedAt"].As().Should().BeCloseTo(newBroadcast.AiredAt); + responseDocument.Data.SingleValue.Attributes["archivedAt"].Should().BeNull(); } [Fact] @@ -457,14 +465,14 @@ public async Task Cannot_create_archived_resource() const string route = "/televisionBroadcasts"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Television broadcasts cannot be created in archived state."); error.Detail.Should().BeNull(); @@ -498,7 +506,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/televisionBroadcasts/" + existingBroadcast.StringId; + string route = $"/televisionBroadcasts/{existingBroadcast.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -541,7 +549,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -586,17 +594,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Archive date of television broadcasts cannot be shifted. Unarchive it first."); error.Detail.Should().BeNull(); @@ -614,7 +622,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -645,17 +653,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/televisionBroadcasts/" + broadcast.StringId; + string route = $"/televisionBroadcasts/{broadcast.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Television broadcasts must first be archived before they can be deleted."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs index 10eed91f1b..862612d978 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving/TelevisionBroadcastDefinition.cs @@ -43,7 +43,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (IsReturningCollectionOfTelevisionBroadcasts() && !HasFilterOnArchivedAt(existingFilter)) { - AttrAttribute archivedAtAttribute = ResourceContext.Attributes.Single(attr => attr.Property.Name == nameof(TelevisionBroadcast.ArchivedAt)); + AttrAttribute archivedAtAttribute = ResourceContext.GetAttributeByPropertyName(nameof(TelevisionBroadcast.ArchivedAt)); var archivedAtChain = new ResourceFieldChainExpression(archivedAtAttribute); FilterExpression isUnarchived = new ComparisonExpression(ComparisonOperator.Equals, archivedAtChain, new NullConstantExpression()); @@ -151,7 +151,7 @@ private static void AssertIsNotArchived(TelevisionBroadcast broadcast) { if (broadcast.ArchivedAt != null) { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) { Title = "Television broadcasts cannot be created in archived state." }); @@ -163,7 +163,7 @@ private void AssertIsNotShiftingArchiveDate(TelevisionBroadcast broadcast) { if (_storedArchivedAt != null && broadcast.ArchivedAt != null && _storedArchivedAt != broadcast.ArchivedAt) { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) { Title = "Archive date of television broadcasts cannot be shifted. Unarchive it first." }); @@ -175,7 +175,7 @@ private static void AssertIsArchived(TelevisionBroadcast broadcast) { if (broadcast.ArchivedAt == null) { - throw new JsonApiException(new Error(HttpStatusCode.Forbidden) + throw new JsonApiException(new ErrorObject(HttpStatusCode.Forbidden) { Title = "Television broadcasts must first be archived before they can be deleted." }); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs index 3651c80368..d00cda4a39 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/AtomicConstrainedOperationsControllerTests.cs @@ -62,8 +62,7 @@ public async Task Can_create_resources_for_matching_resource_type() const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -96,14 +95,14 @@ public async Task Cannot_create_resource_for_mismatching_resource_type() const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); @@ -144,14 +143,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); @@ -199,14 +198,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations/musicTracks/create"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of operation code and resource type at this endpoint."); error.Detail.Should().Be("This endpoint can only be used to create resources of type 'musicTracks'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs index b4b45a4acb..5806456a9c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Controllers/CreateMusicTrackOperationsController.cs @@ -40,11 +40,11 @@ private static void AssertOnlyCreatingMusicTracks(IEnumerable(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(newBornAt); - responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[0].Data.SingleValue.Attributes["bornAt"].As().Should().BeCloseTo(newBornAt); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().BeNull(); - int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -121,8 +120,7 @@ public async Task Can_create_resources() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -131,18 +129,18 @@ public async Task Can_create_resources() for (int index = 0; index < elementCount; index++) { - ResourceObject singleData = responseDocument.Results[index].SingleData; + ResourceObject singleData = responseDocument.Results[index].Data.SingleValue; singleData.Should().NotBeNull(); singleData.Type.Should().Be("musicTracks"); singleData.Attributes["title"].Should().Be(newTracks[index].Title); singleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTracks[index].LengthInSeconds); singleData.Attributes["genre"].Should().Be(newTracks[index].Genre); - singleData.Attributes["releasedAt"].Should().BeCloseTo(newTracks[index].ReleasedAt); + singleData.Attributes["releasedAt"].As().Should().BeCloseTo(newTracks[index].ReleasedAt); singleData.Relationships.Should().NotBeEmpty(); } - Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)).ToArray(); + Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.Data.SingleValue.Id)).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -190,20 +188,19 @@ public async Task Can_create_resource_without_attributes_or_relationships() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(default(DateTimeOffset)); - responseDocument.Results[0].SingleData.Relationships.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["bornAt"].As().Should().BeCloseTo(default); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().BeNull(); - int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -243,19 +240,18 @@ public async Task Can_create_resource_with_unknown_attribute() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newName); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newName); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -285,8 +281,8 @@ public async Task Can_create_resource_with_unknown_relationship() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -298,19 +294,18 @@ public async Task Can_create_resource_with_unknown_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("lyrics"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("lyrics"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newLyricId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -324,7 +319,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_with_client_generated_ID() { // Arrange - string newTitle = _fakers.MusicTrack.Generate().Title; + MusicTrack newTrack = _fakers.MusicTrack.Generate(); + newTrack.Id = Guid.NewGuid(); var requestBody = new { @@ -336,10 +332,10 @@ public async Task Cannot_create_resource_with_client_generated_ID() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = newTrack.StringId, attributes = new { - title = newTitle + title = newTrack.Title } } } @@ -349,14 +345,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Specifying the resource ID in operations that create a resource is not allowed."); error.Detail.Should().BeNull(); @@ -382,14 +378,14 @@ public async Task Cannot_create_resource_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -418,14 +414,14 @@ public async Task Cannot_create_resource_for_ref_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); error.Detail.Should().BeNull(); @@ -450,14 +446,14 @@ public async Task Cannot_create_resource_for_missing_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); @@ -488,14 +484,14 @@ public async Task Cannot_create_resource_for_missing_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -515,7 +511,7 @@ public async Task Cannot_create_resource_for_unknown_type() op = "add", data = new { - type = "doesNotExist" + type = Unknown.ResourceType } } } @@ -524,17 +520,17 @@ public async Task Cannot_create_resource_for_unknown_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -569,14 +565,14 @@ public async Task Cannot_create_resource_for_array() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); error.Detail.Should().BeNull(); @@ -609,14 +605,14 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); error.Detail.Should().Be("Setting the initial value of 'createdAt' is not allowed."); @@ -652,14 +648,14 @@ public async Task Cannot_create_resource_with_readonly_attribute() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' is read-only."); @@ -682,7 +678,7 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() type = "performers", attributes = new { - bornAt = "not-a-valid-time" + bornAt = 12345 } } } @@ -692,18 +688,18 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().Be("Failed to convert attribute 'bornAt' with value '12345' of type 'Number' to type 'DateTimeOffset'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] @@ -774,19 +770,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTitle); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTitle); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs index 6a688dc325..eb3780a140 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs @@ -65,24 +65,25 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + string isoCode = $"{newLanguage.IsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"; + responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("textLanguages"); - responseDocument.Results[0].SingleData.Attributes["isoCode"].Should().Be(newLanguage.IsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("isRightToLeft"); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("textLanguages"); + responseDocument.Results[0].Data.SingleValue.Attributes["isoCode"].Should().Be(isoCode); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("isRightToLeft"); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(newLanguage.Id); - languageInDatabase.IsoCode.Should().Be(newLanguage.IsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); + languageInDatabase.IsoCode.Should().Be(isoCode); }); } @@ -173,14 +174,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Another resource with the specified ID already exists."); error.Detail.Should().Be($"Another resource of type 'textLanguages' with ID '{languageToCreate.StringId}' already exists."); @@ -191,7 +192,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_create_resource_for_incompatible_ID() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; var requestBody = new { @@ -215,14 +216,14 @@ public async Task Cannot_create_resource_for_incompatible_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); @@ -243,7 +244,7 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() data = new { type = "lyrics", - id = 12345678, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -253,14 +254,14 @@ public async Task Cannot_create_resource_for_ID_and_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs index 01168cf1c8..d35107a8b8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToManyRelationshipTests.cs @@ -82,19 +82,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -165,19 +164,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -212,7 +210,7 @@ public async Task Cannot_create_for_missing_relationship_type() { new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -225,14 +223,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'performers' relationship."); @@ -261,8 +259,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -275,17 +273,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -324,14 +322,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -344,6 +342,9 @@ public async Task Cannot_create_for_unknown_relationship_IDs() // Arrange string newTitle = _fakers.MusicTrack.Generate().Title; + string performerId1 = Unknown.StringId.For(); + string performerId2 = Unknown.StringId.AltFor(); + var requestBody = new { atomic__operations = new[] @@ -367,12 +368,12 @@ public async Task Cannot_create_for_unknown_relationship_IDs() new { type = "performers", - id = 12345678 + id = performerId1 }, new { type = "performers", - id = 87654321 + id = performerId2 } } } @@ -385,23 +386,23 @@ public async Task Cannot_create_for_unknown_relationship_IDs() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'performers' with ID '12345678' in relationship 'performers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'performers' with ID '{performerId1}' in relationship 'performers' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'performers' with ID '87654321' in relationship 'performers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'performers' with ID '{performerId2}' in relationship 'performers' does not exist."); error2.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -428,7 +429,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() new { type = "playlists", - id = 12345678 + id = Unknown.StringId.For() } } } @@ -441,14 +442,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); @@ -509,19 +510,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -561,14 +561,14 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -604,14 +604,14 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'tracks' relationship."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs index f00959d331..9153bb404d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithToOneRelationshipTests.cs @@ -3,11 +3,11 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; using TestBuildingBlocks; using Xunit; @@ -71,19 +71,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("lyrics"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("lyrics"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newLyricId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newLyricId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -140,19 +139,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -215,8 +213,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -225,12 +222,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => for (int index = 0; index < elementCount; index++) { - responseDocument.Results[index].SingleData.Should().NotBeNull(); - responseDocument.Results[index].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[index].SingleData.Attributes["title"].Should().Be(newTrackTitles[index]); + responseDocument.Results[index].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[index].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[index].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitles[index]); } - Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.SingleData.Id)).ToArray(); + Guid[] newTrackIds = responseDocument.Results.Select(result => Guid.Parse(result.Data.SingleValue.Id)).ToArray(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -279,7 +276,7 @@ public async Task Cannot_create_for_missing_relationship_type() { data = new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -291,14 +288,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'lyric' relationship."); @@ -325,8 +322,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -338,17 +335,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -384,14 +381,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -402,6 +399,8 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_with_unknown_relationship_ID() { // Arrange + string lyricId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -419,7 +418,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() data = new { type = "lyrics", - id = 12345678 + id = lyricId } } } @@ -431,17 +430,17 @@ public async Task Cannot_create_with_unknown_relationship_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'lyrics' with ID '12345678' in relationship 'lyric' does not exist."); + error.Detail.Should().Be($"Related resource of type 'lyrics' with ID '{lyricId}' in relationship 'lyric' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -466,7 +465,7 @@ public async Task Cannot_create_on_relationship_type_mismatch() data = new { type = "playlists", - id = 12345678 + id = Unknown.StringId.For() } } } @@ -478,14 +477,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); @@ -543,24 +542,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("ownedBy_duplicate", "ownedBy"); + string requestBodyText = JsonSerializer.Serialize(requestBody).Replace("ownedBy_duplicate", "ownedBy"); const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBodyText); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBodyText); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -594,7 +592,7 @@ public async Task Cannot_create_with_data_array_in_relationship() new { type = "lyrics", - id = 12345678 + id = Unknown.StringId.For() } } } @@ -607,14 +605,14 @@ public async Task Cannot_create_with_data_array_in_relationship() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs index 53dd5ee603..ff0f20a15b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Deleting/AtomicDeleteResourceTests.cs @@ -347,14 +347,14 @@ public async Task Cannot_delete_resource_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -379,14 +379,14 @@ public async Task Cannot_delete_resource_for_missing_ref_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref' element is required."); error.Detail.Should().BeNull(); @@ -406,7 +406,7 @@ public async Task Cannot_delete_resource_for_missing_type() op = "remove", @ref = new { - id = 99999999 + id = Unknown.StringId.Int32 } } } @@ -415,14 +415,14 @@ public async Task Cannot_delete_resource_for_missing_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -442,8 +442,8 @@ public async Task Cannot_delete_resource_for_unknown_type() op = "remove", @ref = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -452,17 +452,17 @@ public async Task Cannot_delete_resource_for_unknown_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -488,14 +488,14 @@ public async Task Cannot_delete_resource_for_missing_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -506,6 +506,8 @@ public async Task Cannot_delete_resource_for_missing_ID() public async Task Cannot_delete_resource_for_unknown_ID() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -516,7 +518,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() @ref = new { type = "performers", - id = 99999999 + id = performerId } } } @@ -525,17 +527,17 @@ public async Task Cannot_delete_resource_for_unknown_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -543,7 +545,7 @@ public async Task Cannot_delete_resource_for_unknown_ID() public async Task Cannot_delete_resource_for_incompatible_ID() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; var requestBody = new { @@ -564,14 +566,14 @@ public async Task Cannot_delete_resource_for_incompatible_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int64'."); @@ -592,7 +594,7 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1" } } @@ -602,14 +604,14 @@ public async Task Cannot_delete_resource_for_ID_and_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs index 7e1eefbad6..3f5aadc2b3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ImplicitlyChangingTextLanguageDefinition.cs @@ -29,7 +29,7 @@ public override async Task OnWriteSucceededAsync(TextLanguage resource, WriteOpe { if (writeOperation is not WriteOperationKind.DeleteResource) { - string statement = "Update \"TextLanguages\" SET \"IsoCode\" = '" + resource.IsoCode + Suffix + "' WHERE \"Id\" = '" + resource.Id + "'"; + string statement = $"Update \"TextLanguages\" SET \"IsoCode\" = '{resource.IsoCode}{Suffix}' WHERE \"Id\" = '{resource.StringId}'"; await _dbContext.Database.ExecuteSqlRawAsync(statement, cancellationToken); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs index c33b0b4301..fb3246013a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicAbsoluteLinksTests.cs @@ -80,35 +80,34 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - string languageLink = HostPrefix + "/textLanguages/" + existingLanguage.StringId; + string languageLink = $"{HostPrefix}/textLanguages/{existingLanguage.StringId}"; - ResourceObject singleData1 = responseDocument.Results[0].SingleData; + ResourceObject singleData1 = responseDocument.Results[0].Data.SingleValue; singleData1.Should().NotBeNull(); singleData1.Links.Should().NotBeNull(); singleData1.Links.Self.Should().Be(languageLink); singleData1.Relationships.Should().NotBeEmpty(); singleData1.Relationships["lyrics"].Links.Should().NotBeNull(); - singleData1.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); - singleData1.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); + singleData1.Relationships["lyrics"].Links.Self.Should().Be($"{languageLink}/relationships/lyrics"); + singleData1.Relationships["lyrics"].Links.Related.Should().Be($"{languageLink}/lyrics"); - string companyLink = HostPrefix + "/recordCompanies/" + existingCompany.StringId; + string companyLink = $"{HostPrefix}/recordCompanies/{existingCompany.StringId}"; - ResourceObject singleData2 = responseDocument.Results[1].SingleData; + ResourceObject singleData2 = responseDocument.Results[1].Data.SingleValue; singleData2.Should().NotBeNull(); singleData2.Links.Should().NotBeNull(); singleData2.Links.Self.Should().Be(companyLink); singleData2.Relationships.Should().NotBeEmpty(); singleData2.Relationships["tracks"].Links.Should().NotBeNull(); - singleData2.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); - singleData2.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); + singleData2.Relationships["tracks"].Links.Self.Should().Be($"{companyLink}/relationships/tracks"); + singleData2.Relationships["tracks"].Links.Related.Should().Be($"{companyLink}/tracks"); } [Fact] @@ -145,15 +144,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - ResourceObject singleData = responseDocument.Results[0].SingleData; + ResourceObject singleData = responseDocument.Results[0].Data.SingleValue; singleData.Should().NotBeNull(); singleData.Links.Should().BeNull(); singleData.Relationships.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs index 0a685efa7e..176b7162dc 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Links/AtomicRelativeLinksWithNamespaceTests.cs @@ -70,35 +70,34 @@ public async Task Create_resource_with_side_effects_returns_relative_links() const string route = "/api/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); - string languageLink = "/api/textLanguages/" + Guid.Parse(responseDocument.Results[0].SingleData.Id); + string languageLink = $"/api/textLanguages/{Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id)}"; - responseDocument.Results[0].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Links.Self.Should().Be(languageLink); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Self.Should().Be(languageLink + "/relationships/lyrics"); - responseDocument.Results[0].SingleData.Relationships["lyrics"].Links.Related.Should().Be(languageLink + "/lyrics"); + responseDocument.Results[0].Data.SingleValue.Links.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Links.Self.Should().Be(languageLink); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships["lyrics"].Links.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Relationships["lyrics"].Links.Self.Should().Be($"{languageLink}/relationships/lyrics"); + responseDocument.Results[0].Data.SingleValue.Relationships["lyrics"].Links.Related.Should().Be($"{languageLink}/lyrics"); - responseDocument.Results[1].SingleData.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); - string companyLink = "/api/recordCompanies/" + short.Parse(responseDocument.Results[1].SingleData.Id); + string companyLink = $"/api/recordCompanies/{short.Parse(responseDocument.Results[1].Data.SingleValue.Id)}"; - responseDocument.Results[1].SingleData.Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Links.Self.Should().Be(companyLink); - responseDocument.Results[1].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Self.Should().Be(companyLink + "/relationships/tracks"); - responseDocument.Results[1].SingleData.Relationships["tracks"].Links.Related.Should().Be(companyLink + "/tracks"); + responseDocument.Results[1].Data.SingleValue.Links.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Links.Self.Should().Be(companyLink); + responseDocument.Results[1].Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Results[1].Data.SingleValue.Relationships["tracks"].Links.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Relationships["tracks"].Links.Self.Should().Be($"{companyLink}/relationships/tracks"); + responseDocument.Results[1].Data.SingleValue.Relationships["tracks"].Links.Related.Should().Be($"{companyLink}/tracks"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs index 219d21a808..036cb640af 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/LocalIds/AtomicLocalIdTests.cs @@ -79,27 +79,26 @@ public async Task Can_create_resource_with_ManyToOne_relationship_using_local_ID const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("recordCompanies"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newCompany.Name); - responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(newCompany.CountryOfResidence); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("recordCompanies"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newCompany.Name); + responseDocument.Results[0].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(newCompany.CountryOfResidence); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - short newCompanyId = short.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + short newCompanyId = short.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -173,27 +172,26 @@ public async Task Can_create_resource_with_OneToMany_relationship_using_local_ID const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newPerformer.ArtistName); - responseDocument.Results[0].SingleData.Attributes["bornAt"].Should().BeCloseTo(newPerformer.BornAt); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().Be(newPerformer.ArtistName); + responseDocument.Results[0].Data.SingleValue.Attributes["bornAt"].As().Should().BeCloseTo(newPerformer.BornAt); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - int newPerformerId = int.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + int newPerformerId = int.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -266,26 +264,25 @@ public async Task Can_create_resource_with_ManyToMany_relationship_using_local_I const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - long newPlaylistId = long.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + long newPlaylistId = long.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -315,7 +312,7 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -344,14 +341,14 @@ public async Task Cannot_consume_local_ID_that_is_assigned_in_same_operation() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Local ID cannot be both defined and used within the same operation."); error.Detail.Should().Be("Local ID 'company-1' cannot be both defined and used within the same operation."); @@ -375,7 +372,7 @@ public async Task Cannot_reassign_local_ID() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -410,14 +407,14 @@ public async Task Cannot_reassign_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Another local ID with the same name is already defined at this point."); error.Detail.Should().Be("Another local ID with name 'playlist-1' is already defined at this point."); @@ -469,23 +466,22 @@ public async Task Can_update_resource_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["genre"].Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Attributes["genre"].Should().BeNull(); - responseDocument.Results[1].Data.Should().BeNull(); + responseDocument.Results[1].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -588,34 +584,33 @@ public async Task Can_update_resource_with_relationships_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].SingleData.Should().NotBeNull(); - responseDocument.Results[2].SingleData.Type.Should().Be("recordCompanies"); - responseDocument.Results[2].SingleData.Lid.Should().BeNull(); - responseDocument.Results[2].SingleData.Attributes["name"].Should().Be(newCompanyName); + responseDocument.Results[2].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[2].Data.SingleValue.Type.Should().Be("recordCompanies"); + responseDocument.Results[2].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[2].Data.SingleValue.Attributes["name"].Should().Be(newCompanyName); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); - short newCompanyId = short.Parse(responseDocument.Results[2].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); + short newCompanyId = short.Parse(responseDocument.Results[2].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -702,28 +697,27 @@ public async Task Can_create_ManyToOne_relationship_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("recordCompanies"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newCompanyName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("recordCompanies"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(newCompanyName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - short newCompanyId = short.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + short newCompanyId = short.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -801,28 +795,27 @@ public async Task Can_create_OneToMany_relationship_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -900,28 +893,27 @@ public async Task Can_create_ManyToMany_relationship_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1021,28 +1013,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1142,28 +1133,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1263,28 +1253,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(3); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); - int newPerformerId = int.Parse(responseDocument.Results[1].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); + int newPerformerId = int.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1406,30 +1395,29 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("playlists"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newPlaylistName); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("playlists"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newPlaylistName); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); - Guid newTrackId = Guid.Parse(responseDocument.Results[1].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[1].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1560,32 +1548,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("performers"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["artistName"].Should().Be(newArtistName1); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName1); - responseDocument.Results[1].SingleData.Should().NotBeNull(); - responseDocument.Results[1].SingleData.Type.Should().Be("performers"); - responseDocument.Results[1].SingleData.Lid.Should().BeNull(); - responseDocument.Results[1].SingleData.Attributes["artistName"].Should().Be(newArtistName2); + responseDocument.Results[1].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[1].Data.SingleValue.Type.Should().Be("performers"); + responseDocument.Results[1].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[1].Data.SingleValue.Attributes["artistName"].Should().Be(newArtistName2); - responseDocument.Results[2].SingleData.Should().NotBeNull(); - responseDocument.Results[2].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[2].SingleData.Lid.Should().BeNull(); - responseDocument.Results[2].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[2].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[2].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[2].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[2].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[2].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[2].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1693,24 +1680,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(4); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].Data.Should().BeNull(); + responseDocument.Results[1].Data.Value.Should().BeNull(); - responseDocument.Results[2].Data.Should().BeNull(); + responseDocument.Results[2].Data.Value.Should().BeNull(); - responseDocument.Results[3].Data.Should().BeNull(); + responseDocument.Results[3].Data.Value.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1761,22 +1747,21 @@ public async Task Can_delete_resource_using_local_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Lid.Should().BeNull(); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("musicTracks"); + responseDocument.Results[0].Data.SingleValue.Lid.Should().BeNull(); + responseDocument.Results[0].Data.SingleValue.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[1].Data.Should().BeNull(); + responseDocument.Results[1].Data.Value.Should().BeNull(); - Guid newTrackId = Guid.Parse(responseDocument.Results[0].SingleData.Id); + Guid newTrackId = Guid.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1800,7 +1785,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1809,7 +1794,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() @ref = new { type = "musicTracks", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -1818,17 +1803,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -1846,7 +1831,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1855,7 +1840,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() data = new { type = "musicTracks", - lid = "doesNotExist", + lid = Unknown.LocalId, attributes = new { } @@ -1867,17 +1852,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_data_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -1903,7 +1888,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1920,7 +1905,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "performers", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -1930,17 +1915,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -1958,7 +1943,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -1974,7 +1959,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen data = new { type = "recordCompanies", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -1986,17 +1971,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_elemen const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -2014,7 +1999,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2032,7 +2017,7 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( new { type = "musicTracks", - lid = "doesNotExist" + lid = Unknown.LocalId } } } @@ -2045,17 +2030,17 @@ public async Task Cannot_consume_unassigned_local_ID_in_relationship_data_array( const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Server-generated value for local ID is not available at this point."); - error.Detail.Should().Be("Server-generated value for local ID 'doesNotExist' is not available at this point."); + error.Detail.Should().Be($"Server-generated value for local ID '{Unknown.LocalId}' is not available at this point."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); } @@ -2075,7 +2060,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2104,14 +2089,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_same_operation() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'track-1' belongs to resource type 'musicTracks' instead of 'recordCompanies'."); @@ -2134,7 +2119,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2161,14 +2146,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'musicTracks'."); @@ -2191,7 +2176,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2221,14 +2206,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_data_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'playlists'."); @@ -2259,7 +2244,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2295,14 +2280,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'company-1' belongs to resource type 'recordCompanies' instead of 'performers'."); @@ -2327,7 +2312,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2368,14 +2353,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'playlist-1' belongs to resource type 'playlists' instead of 'recordCompanies'."); @@ -2398,7 +2383,7 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data @ref = new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } }, new @@ -2438,14 +2423,14 @@ public async Task Cannot_consume_local_ID_of_different_type_in_relationship_data const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Type mismatch in local ID usage."); error.Detail.Should().Be("Local ID 'performer-1' belongs to resource type 'performers' instead of 'musicTracks'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs index 8c6dc402ec..a13c92eb2c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Extensions; @@ -79,19 +80,18 @@ public async Task Returns_resource_meta_in_create_resource_with_side_effects() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta["Copyright"].Should().Be("(C) 2018. All rights reserved."); + responseDocument.Results[0].Data.SingleValue.Meta.Should().HaveCount(1); + ((JsonElement)responseDocument.Results[0].Data.SingleValue.Meta["copyright"]).GetString().Should().Be("(C) 2018. All rights reserved."); - responseDocument.Results[1].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[1].SingleData.Meta["Copyright"].Should().Be("(C) 1994. All rights reserved."); + responseDocument.Results[1].Data.SingleValue.Meta.Should().HaveCount(1); + ((JsonElement)responseDocument.Results[1].Data.SingleValue.Meta["copyright"]).GetString().Should().Be("(C) 1994. All rights reserved."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -136,15 +136,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Meta["Notice"].Should().Be(TextLanguageMetaDefinition.NoticeText); + responseDocument.Results[0].Data.SingleValue.Meta.Should().HaveCount(1); + ((JsonElement)responseDocument.Results[0].Data.SingleValue.Meta["notice"]).GetString().Should().Be(TextLanguageMetaDefinition.NoticeText); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs index 41b1f43567..f775ea6c79 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResponseMetaTests.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json.Linq; using TestBuildingBlocks; using Xunit; @@ -58,17 +57,16 @@ public async Task Returns_top_level_meta_in_create_resource_with_side_effects() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().HaveCount(3); - responseDocument.Meta["license"].Should().Be("MIT"); - responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); + ((JsonElement)responseDocument.Meta["license"]).GetString().Should().Be("MIT"); + ((JsonElement)responseDocument.Meta["projectUrl"]).GetString().Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); - string[] versionArray = ((IEnumerable)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); + string[] versionArray = ((JsonElement)responseDocument.Meta["versions"]).EnumerateArray().Select(element => element.GetString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); @@ -111,17 +109,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().HaveCount(3); - responseDocument.Meta["license"].Should().Be("MIT"); - responseDocument.Meta["projectUrl"].Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); + ((JsonElement)responseDocument.Meta["license"]).GetString().Should().Be("MIT"); + ((JsonElement)responseDocument.Meta["projectUrl"]).GetString().Should().Be("https://github.com/json-api-dotnet/JsonApiDotNetCore/"); - string[] versionArray = ((IEnumerable)responseDocument.Meta["versions"]).Select(token => token.ToString()).ToArray(); + string[] versionArray = ((JsonElement)responseDocument.Meta["versions"]).EnumerateArray().Select(element => element.GetString()).ToArray(); versionArray.Should().HaveCount(4); versionArray.Should().Contain("v4.0.0"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs index 42e18e0ba4..8b2a56a092 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicRequestBodyTests.cs @@ -29,18 +29,18 @@ public async Task Cannot_process_for_missing_request_body() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, null); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); - error.Source.Pointer.Should().BeNull(); + error.Source.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -61,18 +61,18 @@ public async Task Cannot_process_for_broken_JSON_request_body() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Unexpected end of content while loading JObject."); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().Match("* There is an open JSON object or array that should be closed. *"); + error.Source.Should().BeNull(); } [Fact] @@ -87,18 +87,18 @@ public async Task Cannot_process_empty_operations_array() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: No operations found."); error.Detail.Should().BeNull(); - error.Source.Pointer.Should().BeNull(); + error.Source.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -135,18 +135,18 @@ public async Task Cannot_process_for_unknown_operation_code() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Error converting value \"merge\" to type"); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().StartWith("The JSON value could not be converted to "); + error.Source.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs index a47557ab7d..bc9b637617 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/AtomicSerializationTests.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -12,6 +13,8 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations.Mixed { public sealed class AtomicSerializationTests : IClassFixture, OperationsDbContext>> { + private const string JsonDateTimeOffsetFormatSpecifier = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK"; + private readonly IntegrationTestContext, OperationsDbContext> _testContext; private readonly OperationsFakers _fakers = new(); @@ -30,6 +33,7 @@ public AtomicSerializationTests(IntegrationTestContext(); + options.IncludeExceptionStackTraceInErrors = false; options.IncludeJsonApiVersion = true; options.AllowClientGeneratedIds = true; } @@ -38,8 +42,8 @@ public AtomicSerializationTests(IntegrationTestContext { @@ -56,10 +60,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "performers", - id = newArtistId, + id = newPerformer.StringId, attributes = new { - artistName = newArtistName + artistName = newPerformer.ArtistName, + bornAt = newPerformer.BornAt } } } @@ -85,14 +90,67 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { ""data"": { ""type"": ""performers"", - ""id"": """ + newArtistId + @""", + ""id"": """ + newPerformer.StringId + @""", ""attributes"": { - ""artistName"": """ + newArtistName + @""", - ""bornAt"": ""0001-01-01T01:00:00+01:00"" + ""artistName"": """ + newPerformer.ArtistName + @""", + ""bornAt"": """ + newPerformer.BornAt.ToString(JsonDateTimeOffsetFormatSpecifier) + @""" }, ""links"": { - ""self"": ""http://localhost/performers/" + newArtistId + @""" + ""self"": ""http://localhost/performers/" + newPerformer.StringId + @""" + } + } + } + ] +}"); } + + [Fact] + public async Task Includes_version_with_ext_on_error_in_operations_endpoint() + { + // Arrange + string musicTrackId = Unknown.StringId.For(); + + var requestBody = new + { + atomic__operations = new[] + { + new + { + op = "remove", + @ref = new + { + type = "musicTracks", + id = musicTrackId + } + } + } + }; + + const string route = "/operations"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); + + responseDocument.Should().BeJson(@"{ + ""jsonapi"": { + ""version"": ""1.1"", + ""ext"": [ + ""https://jsonapi.org/ext/atomic"" + ] + }, + ""errors"": [ + { + ""id"": """ + errorId + @""", + ""status"": ""404"", + ""title"": ""The requested resource does not exist."", + ""detail"": ""Resource of type 'musicTracks' with ID '" + musicTrackId + @"' does not exist."", + ""source"": { + ""pointer"": ""/atomic:operations[0]"" } } ] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs index a48c2424cd..d72a79f9b8 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Mixed/MaximumOperationsPerRequestTests.cs @@ -38,6 +38,7 @@ public async Task Cannot_process_more_operations_than_maximum() op = "add", data = new { + type = "performers" } }, new @@ -45,6 +46,7 @@ public async Task Cannot_process_more_operations_than_maximum() op = "remove", data = new { + type = "performers" } }, new @@ -52,6 +54,7 @@ public async Task Cannot_process_more_operations_than_maximum() op = "update", data = new { + type = "performers" } } } @@ -60,18 +63,18 @@ public async Task Cannot_process_more_operations_than_maximum() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request exceeds the maximum number of operations."); error.Detail.Should().Be("The number of operations in this request (3) is higher than 2."); - error.Source.Pointer.Should().BeNull(); + error.Source.Should().BeNull(); } [Fact] @@ -113,7 +116,7 @@ public async Task Can_process_operations_same_as_maximum() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -153,7 +156,7 @@ public async Task Can_process_high_number_of_operations_when_unconstrained() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs index 706c990642..39b43ecf01 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs @@ -49,20 +49,20 @@ public async Task Cannot_create_resource_with_multiple_violations() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); @@ -118,15 +118,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - long newPlaylistId = long.Parse(responseDocument.Results[0].SingleData.Id); + long newPlaylistId = long.Parse(responseDocument.Results[0].Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -173,20 +172,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Title field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/title"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); @@ -432,7 +431,7 @@ public async Task Validates_all_operations_before_execution_starts() data = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), attributes = new { name = (string)null @@ -457,26 +456,26 @@ public async Task Validates_all_operations_before_execution_starts() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(3); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Name field is required."); error1.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/name"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The Title field is required."); error2.Source.Pointer.Should().Be("/atomic:operations[1]/data/attributes/title"); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error3.Title.Should().Be("Input validation failed."); error3.Detail.Should().Be("The field LengthInSeconds must be between 1 and 1440."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs index 3c19514876..cd63f41dd1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/QueryStrings/AtomicQueryStringTests.cs @@ -37,10 +37,6 @@ public AtomicQueryStringTests(IntegrationTestContext(); }); - - var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerDefaultValueHandling = true; - options.AllowQueryStringOverrideForSerializerNullValueHandling = true; } [Fact] @@ -68,14 +64,14 @@ public async Task Cannot_include_on_operations_endpoint() const string route = "/operations?include=recordCompanies"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'include' cannot be used at this endpoint."); @@ -107,14 +103,14 @@ public async Task Cannot_filter_on_operations_endpoint() const string route = "/operations?filter=equals(id,'1')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'filter' cannot be used at this endpoint."); @@ -146,14 +142,14 @@ public async Task Cannot_sort_on_operations_endpoint() const string route = "/operations?sort=-id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); @@ -185,14 +181,14 @@ public async Task Cannot_use_pagination_number_on_operations_endpoint() const string route = "/operations?page[number]=1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); @@ -224,14 +220,14 @@ public async Task Cannot_use_pagination_size_on_operations_endpoint() const string route = "/operations?page[size]=1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[size]' cannot be used at this endpoint."); @@ -263,14 +259,14 @@ public async Task Cannot_use_sparse_fieldset_on_operations_endpoint() const string route = "/operations?fields[recordCompanies]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'fields[recordCompanies]' cannot be used at this endpoint."); @@ -301,8 +297,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(musicTracks[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(musicTracks[2].StringId); } [Fact] @@ -333,14 +329,14 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() const string route = "/operations?isRecentlyReleased=true"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Unknown query string parameter."); @@ -349,93 +345,5 @@ public async Task Cannot_use_Queryable_handler_on_operations_endpoint() error.Source.Parameter.Should().Be("isRecentlyReleased"); } - - [Fact] - public async Task Can_use_defaults_on_operations_endpoint() - { - // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; - decimal? newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; - - var requestBody = new - { - atomic__operations = new object[] - { - new - { - op = "add", - data = new - { - type = "musicTracks", - attributes = new - { - title = newTrackTitle, - lengthInSeconds = newTrackLength - } - } - } - } - }; - - const string route = "/operations?defaults=false"; - - // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength); - } - - [Fact] - public async Task Can_use_nulls_on_operations_endpoint() - { - // Arrange - string newTrackTitle = _fakers.MusicTrack.Generate().Title; - decimal? newTrackLength = _fakers.MusicTrack.Generate().LengthInSeconds; - - var requestBody = new - { - atomic__operations = new object[] - { - new - { - op = "add", - data = new - { - type = "musicTracks", - attributes = new - { - title = newTrackTitle, - lengthInSeconds = newTrackLength - } - } - } - } - }; - - const string route = "/operations?nulls=false"; - - // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("musicTracks"); - responseDocument.Results[0].SingleData.Attributes.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["title"].Should().Be(newTrackTitle); - responseDocument.Results[0].SingleData.Attributes["lengthInSeconds"].As().Should().BeApproximately(newTrackLength); - } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs index 33a9055cf1..fbde96e586 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/Serialization/AtomicSerializationResourceDefinitionTests.cs @@ -86,19 +86,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(newCompanies[0].Name.ToUpperInvariant()); - responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(newCompanies[0].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(newCompanies[0].Name.ToUpperInvariant()); + responseDocument.Results[0].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(newCompanies[0].CountryOfResidence.ToUpperInvariant()); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(newCompanies[1].Name.ToUpperInvariant()); - responseDocument.Results[1].SingleData.Attributes["countryOfResidence"].Should().Be(newCompanies[1].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(newCompanies[1].Name.ToUpperInvariant()); + responseDocument.Results[1].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(newCompanies[1].CountryOfResidence.ToUpperInvariant()); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -170,8 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -230,19 +228,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Results.Should().HaveCount(2); + string country0 = existingCompanies[0].CountryOfResidence.ToUpperInvariant(); + string country1 = existingCompanies[1].CountryOfResidence.ToUpperInvariant(); - responseDocument.Results[0].SingleData.Attributes["name"].Should().Be(existingCompanies[0].Name); - responseDocument.Results[0].SingleData.Attributes["countryOfResidence"].Should().Be(existingCompanies[0].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[1].SingleData.Attributes["name"].Should().Be(existingCompanies[1].Name); - responseDocument.Results[1].SingleData.Attributes["countryOfResidence"].Should().Be(existingCompanies[1].CountryOfResidence.ToUpperInvariant()); + responseDocument.Results[0].Data.SingleValue.Attributes["name"].Should().Be(existingCompanies[0].Name); + responseDocument.Results[0].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(country0); + responseDocument.Results[1].Data.SingleValue.Attributes["name"].Should().Be(existingCompanies[1].Name); + responseDocument.Results[1].Data.SingleValue.Attributes["countryOfResidence"].Should().Be(country1); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -313,8 +312,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs index 620a0b638e..4c6df1fc48 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ResourceDefinitions/SparseFieldSets/AtomicSparseFieldSetResourceDefinitionTests.cs @@ -84,19 +84,18 @@ public async Task Hides_text_in_create_resource_with_side_effects() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["format"].Should().Be(newLyrics[0].Format); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[0].Data.SingleValue.Attributes["format"].Should().Be(newLyrics[0].Format); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("text"); - responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(newLyrics[1].Format); - responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[1].Data.SingleValue.Attributes["format"].Should().Be(newLyrics[1].Format); + responseDocument.Results[1].Data.SingleValue.Attributes.Should().NotContainKey("text"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -158,19 +157,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(2); - responseDocument.Results[0].SingleData.Attributes["format"].Should().Be(existingLyrics[0].Format); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[0].Data.SingleValue.Attributes["format"].Should().Be(existingLyrics[0].Format); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("text"); - responseDocument.Results[1].SingleData.Attributes["format"].Should().Be(existingLyrics[1].Format); - responseDocument.Results[1].SingleData.Attributes.Should().NotContainKey("text"); + responseDocument.Results[1].Data.SingleValue.Attributes["format"].Should().Be(existingLyrics[1].Format); + responseDocument.Results[1].Data.SingleValue.Attributes.Should().NotContainKey("text"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs index b6ff66af7a..542a6971b3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicRollbackTests.cs @@ -36,6 +36,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.ClearTablesAsync(); }); + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new object[] @@ -72,7 +74,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "performers", - id = 99999999 + id = performerId } } } @@ -85,17 +87,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'performers' with ID '99999999' in relationship 'performers' does not exist."); + error.Detail.Should().Be($"Related resource of type 'performers' with ID '{performerId}' in relationship 'performers' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[1]"); await _testContext.RunOnDatabaseAsync(async dbContext => diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs index 8029a7481f..245af3c761 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Transactions/AtomicTransactionConsistencyTests.cs @@ -60,14 +60,14 @@ public async Task Cannot_use_non_transactional_repository() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported resource type in atomic:operations request."); error.Detail.Should().Be("Operations on resources of type 'performers' cannot be used because transaction support is unavailable."); @@ -99,14 +99,14 @@ public async Task Cannot_use_transactional_repository_without_active_transaction const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); @@ -138,14 +138,14 @@ public async Task Cannot_use_distributed_transaction() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Unsupported combination of resource types in atomic:operations request."); error.Detail.Should().Be("All operations need to participate in a single shared transaction, which is not the case for this request."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs index c095e6ec7e..ada44f47e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicAddToToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -62,14 +61,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'add' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); @@ -254,14 +253,14 @@ public async Task Cannot_add_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -281,7 +280,7 @@ public async Task Cannot_add_for_missing_type_in_ref() op = "add", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -291,14 +290,14 @@ public async Task Cannot_add_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -318,8 +317,8 @@ public async Task Cannot_add_for_unknown_type_in_ref() op = "add", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -329,17 +328,17 @@ public async Task Cannot_add_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -366,14 +365,14 @@ public async Task Cannot_add_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -392,6 +391,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string companyId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -402,7 +403,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "recordCompanies", - id = 9999, + id = companyId, relationship = "tracks" }, data = new[] @@ -420,17 +421,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Detail.Should().Be($"Resource of type 'recordCompanies' with ID '{companyId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -448,7 +449,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "performers" } @@ -459,14 +460,14 @@ public async Task Cannot_add_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -486,8 +487,8 @@ public async Task Cannot_add_for_missing_relationship_in_ref() op = "add", @ref = new { - id = 99999999, - type = "musicTracks" + type = "musicTracks", + id = Unknown.StringId.For() } } } @@ -496,14 +497,14 @@ public async Task Cannot_add_for_missing_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.relationship' element is required."); error.Detail.Should().BeNull(); @@ -524,8 +525,8 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -534,17 +535,17 @@ public async Task Cannot_add_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -581,14 +582,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -609,14 +610,14 @@ public async Task Cannot_add_for_missing_type_in_data() @ref = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" }, data = new[] { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -626,14 +627,14 @@ public async Task Cannot_add_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -654,15 +655,15 @@ public async Task Cannot_add_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -672,17 +673,17 @@ public async Task Cannot_add_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -700,7 +701,7 @@ public async Task Cannot_add_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -717,14 +718,14 @@ public async Task Cannot_add_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -745,7 +746,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -753,7 +754,7 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -764,14 +765,14 @@ public async Task Cannot_add_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -783,7 +784,12 @@ public async Task Cannot_add_for_unknown_IDs_in_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -809,12 +815,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -824,20 +830,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -874,7 +880,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -884,14 +890,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs index 627dfe404e..ffaff765c3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicRemoveFromToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -63,14 +62,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Only to-many relationships can be targeted in 'remove' operations."); error.Detail.Should().Be("Relationship 'ownedBy' must be a to-many relationship."); @@ -254,14 +253,14 @@ public async Task Cannot_remove_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -281,7 +280,7 @@ public async Task Cannot_remove_for_missing_type_in_ref() op = "remove", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -291,14 +290,14 @@ public async Task Cannot_remove_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -318,8 +317,8 @@ public async Task Cannot_remove_for_unknown_type_in_ref() op = "remove", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -329,17 +328,17 @@ public async Task Cannot_remove_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -366,14 +365,14 @@ public async Task Cannot_remove_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -392,6 +391,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string companyId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -402,7 +403,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "recordCompanies", - id = 9999, + id = companyId, relationship = "tracks" }, data = new[] @@ -420,17 +421,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Detail.Should().Be($"Resource of type 'recordCompanies' with ID '{companyId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -448,7 +449,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "performers" } @@ -459,14 +460,14 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -487,8 +488,8 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -497,17 +498,17 @@ public async Task Cannot_remove_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -544,14 +545,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -572,14 +573,14 @@ public async Task Cannot_remove_for_missing_type_in_data() @ref = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" }, data = new[] { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -589,14 +590,14 @@ public async Task Cannot_remove_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -617,15 +618,15 @@ public async Task Cannot_remove_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -635,17 +636,17 @@ public async Task Cannot_remove_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -663,7 +664,7 @@ public async Task Cannot_remove_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -680,14 +681,14 @@ public async Task Cannot_remove_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -708,7 +709,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -716,7 +717,7 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -727,14 +728,14 @@ public async Task Cannot_remove_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -746,7 +747,12 @@ public async Task Cannot_remove_for_unknown_IDs_in_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -772,12 +778,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -787,20 +793,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -837,7 +843,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -847,14 +853,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs index 69c02d0fa6..c1426db8a9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicReplaceToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -291,14 +290,14 @@ public async Task Cannot_replace_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -318,7 +317,7 @@ public async Task Cannot_replace_for_missing_type_in_ref() op = "update", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -328,14 +327,14 @@ public async Task Cannot_replace_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -355,8 +354,8 @@ public async Task Cannot_replace_for_unknown_type_in_ref() op = "update", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "tracks" } } @@ -366,17 +365,17 @@ public async Task Cannot_replace_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -403,14 +402,14 @@ public async Task Cannot_replace_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -429,6 +428,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string companyId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -439,7 +440,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "recordCompanies", - id = 9999, + id = companyId, relationship = "tracks" }, data = new[] @@ -457,17 +458,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'recordCompanies' with ID '9999' does not exist."); + error.Detail.Should().Be($"Resource of type 'recordCompanies' with ID '{companyId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -475,7 +476,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_replace_for_incompatible_ID_in_ref() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; MusicTrack existingTrack = _fakers.MusicTrack.Generate(); @@ -513,14 +514,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int16'."); @@ -541,7 +542,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "performers" } @@ -552,14 +553,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -580,8 +581,8 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -590,17 +591,17 @@ public async Task Cannot_replace_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -637,14 +638,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -665,14 +666,14 @@ public async Task Cannot_replace_for_missing_type_in_data() @ref = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationship = "tracks" }, data = new[] { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -682,14 +683,14 @@ public async Task Cannot_replace_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].type' element is required."); error.Detail.Should().BeNull(); @@ -710,15 +711,15 @@ public async Task Cannot_replace_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -728,17 +729,17 @@ public async Task Cannot_replace_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -756,7 +757,7 @@ public async Task Cannot_replace_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -773,14 +774,14 @@ public async Task Cannot_replace_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -801,7 +802,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "performers" }, data = new[] @@ -809,7 +810,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -820,14 +821,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data[].id' or 'data[].lid' element is required."); error.Detail.Should().BeNull(); @@ -839,7 +840,12 @@ public async Task Cannot_replace_for_unknown_IDs_in_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -865,12 +871,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -880,20 +886,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -940,14 +946,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -984,7 +990,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -994,14 +1000,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data[].type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data[].type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs index 5471ab1add..85c153a7f1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Relationships/AtomicUpdateToOneRelationshipTests.cs @@ -537,14 +537,14 @@ public async Task Cannot_create_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -564,7 +564,7 @@ public async Task Cannot_create_for_missing_type_in_ref() op = "update", @ref = new { - id = 99999999, + id = Unknown.StringId.For(), relationship = "track" } } @@ -574,14 +574,14 @@ public async Task Cannot_create_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -601,8 +601,8 @@ public async Task Cannot_create_for_unknown_type_in_ref() op = "update", @ref = new { - type = "doesNotExist", - id = 99999999, + type = Unknown.ResourceType, + id = Unknown.StringId.For(), relationship = "ownedBy" } } @@ -612,17 +612,17 @@ public async Task Cannot_create_for_unknown_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -649,14 +649,14 @@ public async Task Cannot_create_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -667,7 +667,7 @@ public async Task Cannot_create_for_missing_ID_in_ref() public async Task Cannot_create_for_unknown_ID_in_ref() { // Arrange - string missingTrackId = Guid.NewGuid().ToString(); + string trackId = Unknown.StringId.For(); Lyric existingLyric = _fakers.Lyric.Generate(); @@ -687,7 +687,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => @ref = new { type = "musicTracks", - id = missingTrackId, + id = trackId, relationship = "lyric" }, data = new @@ -702,17 +702,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be($"Resource of type 'musicTracks' with ID '{missingTrackId}' does not exist."); + error.Detail.Should().Be($"Resource of type 'musicTracks' with ID '{trackId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -753,14 +753,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -781,7 +781,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), lid = "local-1", relationship = "ownedBy" } @@ -792,14 +792,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -820,8 +820,8 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() @ref = new { type = "performers", - id = 99999999, - relationship = "doesNotExist" + id = Unknown.StringId.For(), + relationship = Unknown.Relationship } } } @@ -830,17 +830,17 @@ public async Task Cannot_create_for_unknown_relationship_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The referenced relationship does not exist."); - error.Detail.Should().Be("Resource of type 'performers' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'performers' does not contain a relationship named '{Unknown.Relationship}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -874,7 +874,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -884,14 +884,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); @@ -912,12 +912,12 @@ public async Task Cannot_create_for_missing_type_in_data() @ref = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), relationship = "track" }, data = new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -926,14 +926,14 @@ public async Task Cannot_create_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -954,13 +954,13 @@ public async Task Cannot_create_for_unknown_type_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "lyric" }, data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -969,17 +969,17 @@ public async Task Cannot_create_for_unknown_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -997,7 +997,7 @@ public async Task Cannot_create_for_missing_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "lyric" }, data = new @@ -1011,14 +1011,14 @@ public async Task Cannot_create_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -1039,13 +1039,13 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() @ref = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationship = "lyric" }, data = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -1055,14 +1055,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -1081,6 +1081,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string lyricId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1097,7 +1099,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "lyrics", - id = 99999999 + id = lyricId } } } @@ -1106,17 +1108,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); + error.Detail.Should().Be($"Related resource of type 'lyrics' with ID '{lyricId}' in relationship 'lyric' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1157,14 +1159,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be("Failed to convert 'invalid-guid' of type 'String' to type 'Guid'."); @@ -1199,7 +1201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "playlists", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -1208,14 +1210,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.relationship' and 'data.type' element."); error.Detail.Should().Be("Expected resource of type 'lyrics' in 'data.type', instead of 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs index 736ffd8646..3ee9307112 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicReplaceToManyRelationshipTests.cs @@ -4,7 +4,6 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using JsonApiDotNetCore; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using TestBuildingBlocks; @@ -330,14 +329,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().Be("Expected data[] element for 'performers' relationship."); @@ -358,7 +357,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() data = new { type = "playlists", - id = 99999999, + id = Unknown.StringId.For(), relationships = new { tracks = new @@ -367,7 +366,7 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() { new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -380,14 +379,14 @@ public async Task Cannot_replace_for_missing_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'tracks' relationship."); @@ -408,7 +407,7 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { performers = new @@ -417,8 +416,8 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -431,17 +430,17 @@ public async Task Cannot_replace_for_unknown_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -459,7 +458,7 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { performers = new @@ -481,14 +480,14 @@ public async Task Cannot_replace_for_missing_ID_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -509,7 +508,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { performers = new @@ -519,7 +518,7 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() new { type = "performers", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -533,14 +532,14 @@ public async Task Cannot_replace_for_ID_and_local_ID_relationship_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'performers' relationship."); @@ -552,7 +551,12 @@ public async Task Cannot_replace_for_unknown_IDs_in_relationship_data() { // Arrange RecordCompany existingCompany = _fakers.RecordCompany.Generate(); - Guid[] trackIds = ArrayFactory.Create(Guid.NewGuid(), Guid.NewGuid()); + + string[] trackIds = + { + Unknown.StringId.For(), + Unknown.StringId.AltFor() + }; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -580,12 +584,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "musicTracks", - id = trackIds[0].ToString() + id = trackIds[0] }, new { type = "musicTracks", - id = trackIds[1].ToString() + id = trackIds[1] } } } @@ -598,20 +602,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); error1.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[0]}' in relationship 'tracks' does not exist."); error1.Source.Pointer.Should().Be("/atomic:operations[0]"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); error2.Detail.Should().Be($"Related resource of type 'musicTracks' with ID '{trackIds[1]}' in relationship 'tracks' does not exist."); @@ -650,7 +654,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "playlists", - id = 88888888 + id = Unknown.StringId.For() } } } @@ -663,14 +667,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'performers' contains incompatible resource type 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs index 391d2bafd6..1a6b73e239 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs @@ -238,8 +238,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -419,24 +419,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Type.Should().Be("textLanguages"); - responseDocument.Results[0].SingleData.Attributes["isoCode"].Should().Be(newIsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); - responseDocument.Results[0].SingleData.Attributes.Should().NotContainKey("isRightToLeft"); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Type.Should().Be("textLanguages"); + responseDocument.Results[0].Data.SingleValue.Attributes["isoCode"].Should().Be($"{newIsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"); + responseDocument.Results[0].Data.SingleValue.Attributes.Should().NotContainKey("isRightToLeft"); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { TextLanguage languageInDatabase = await dbContext.TextLanguages.FirstWithIdAsync(existingLanguage.Id); - languageInDatabase.IsoCode.Should().Be(newIsoCode + ImplicitlyChangingTextLanguageDefinition.Suffix); + languageInDatabase.IsoCode.Should().Be($"{newIsoCode}{ImplicitlyChangingTextLanguageDefinition.Suffix}"); }); } @@ -472,16 +471,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, AtomicOperationsDocument responseDocument) = - await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Results.Should().HaveCount(1); - responseDocument.Results[0].SingleData.Should().NotBeNull(); - responseDocument.Results[0].SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.Results[0].SingleData.Relationships.Values.Should().OnlyContain(relationshipEntry => relationshipEntry.Data == null); + responseDocument.Results[0].Data.SingleValue.Should().NotBeNull(); + responseDocument.Results[0].Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Results[0].Data.SingleValue.Relationships.Values.Should().OnlyContain(relationshipObject => relationshipObject.Data.Value == null); } [Fact] @@ -503,14 +501,14 @@ public async Task Cannot_update_resource_for_href_element() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Usage of the 'href' element is not supported."); error.Detail.Should().BeNull(); @@ -587,12 +585,12 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() op = "update", @ref = new { - id = 12345678 + id = Unknown.StringId.For() }, data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), attributes = new { }, @@ -607,14 +605,14 @@ public async Task Cannot_update_resource_for_missing_type_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.type' element is required."); error.Detail.Should().BeNull(); @@ -639,7 +637,7 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), attributes = new { }, @@ -654,14 +652,14 @@ public async Task Cannot_update_resource_for_missing_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -682,13 +680,13 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() @ref = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), lid = "local-1" }, data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.AltFor(), attributes = new { }, @@ -703,14 +701,14 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_ref() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'ref.id' or 'ref.lid' element is required."); error.Detail.Should().BeNull(); @@ -735,14 +733,14 @@ public async Task Cannot_update_resource_for_missing_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data' element is required."); error.Detail.Should().BeNull(); @@ -762,7 +760,7 @@ public async Task Cannot_update_resource_for_missing_type_in_data() op = "update", data = new { - id = 12345678, + id = Unknown.StringId.Int32, attributes = new { }, @@ -777,14 +775,14 @@ public async Task Cannot_update_resource_for_missing_type_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.type' element is required."); error.Detail.Should().BeNull(); @@ -819,14 +817,14 @@ public async Task Cannot_update_resource_for_missing_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -847,7 +845,7 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() data = new { type = "performers", - id = 12345678, + id = Unknown.StringId.For(), lid = "local-1", attributes = new { @@ -863,14 +861,14 @@ public async Task Cannot_update_resource_for_ID_and_local_ID_in_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: The 'data.id' or 'data.lid' element is required."); error.Detail.Should().BeNull(); @@ -915,14 +913,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for create/update resource operation."); error.Detail.Should().BeNull(); @@ -943,12 +941,12 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() @ref = new { type = "performers", - id = 12345678 + id = Unknown.StringId.For() }, data = new { type = "playlists", - id = 12345678, + id = Unknown.StringId.For(), attributes = new { }, @@ -963,14 +961,14 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource type mismatch between 'ref.type' and 'data.type' element."); error.Detail.Should().Be("Expected resource of type 'performers' in 'data.type', instead of 'playlists'."); @@ -981,6 +979,9 @@ public async Task Cannot_update_on_resource_type_mismatch_between_ref_and_data() public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() { // Arrange + string performerId1 = Unknown.StringId.For(); + string performerId2 = Unknown.StringId.AltFor(); + var requestBody = new { atomic__operations = new[] @@ -991,12 +992,12 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() @ref = new { type = "performers", - id = 12345678 + id = performerId1 }, data = new { type = "performers", - id = 87654321, + id = performerId2, attributes = new { }, @@ -1011,17 +1012,17 @@ public async Task Cannot_update_on_resource_ID_mismatch_between_ref_and_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource ID mismatch between 'ref.id' and 'data.id' element."); - error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of '87654321'."); + error.Detail.Should().Be($"Expected resource with ID '{performerId1}' in 'data.id', instead of '{performerId2}'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1059,14 +1060,14 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource local ID mismatch between 'ref.lid' and 'data.lid' element."); error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of 'local-2'."); @@ -1077,6 +1078,8 @@ public async Task Cannot_update_on_resource_local_ID_mismatch_between_ref_and_da public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_data() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1087,7 +1090,7 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da @ref = new { type = "performers", - id = "12345678" + id = performerId }, data = new { @@ -1107,17 +1110,17 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.id' and 'data.lid' element."); - error.Detail.Should().Be("Expected resource with ID '12345678' in 'data.id', instead of 'local-1' in 'data.lid'."); + error.Detail.Should().Be($"Expected resource with ID '{performerId}' in 'data.id', instead of 'local-1' in 'data.lid'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1125,6 +1128,8 @@ public async Task Cannot_update_on_mixture_of_ID_and_local_ID_between_ref_and_da public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_data() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1140,7 +1145,7 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da data = new { type = "performers", - id = "12345678", + id = performerId, attributes = new { }, @@ -1155,17 +1160,17 @@ public async Task Cannot_update_on_mixture_of_local_ID_and_ID_between_ref_and_da const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Resource identity mismatch between 'ref.lid' and 'data.id' element."); - error.Detail.Should().Be("Expected resource with local ID 'local-1' in 'data.lid', instead of '12345678' in 'data.id'."); + error.Detail.Should().Be($"Expected resource with local ID 'local-1' in 'data.lid', instead of '{performerId}' in 'data.id'."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1182,8 +1187,8 @@ public async Task Cannot_update_resource_for_unknown_type() op = "update", data = new { - type = "doesNotExist", - id = 12345678, + type = Unknown.ResourceType, + id = Unknown.StringId.Int32, attributes = new { }, @@ -1198,17 +1203,17 @@ public async Task Cannot_update_resource_for_unknown_type() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1216,6 +1221,8 @@ public async Task Cannot_update_resource_for_unknown_type() public async Task Cannot_update_resource_for_unknown_ID() { // Arrange + string performerId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -1226,7 +1233,7 @@ public async Task Cannot_update_resource_for_unknown_ID() data = new { type = "performers", - id = 99999999, + id = performerId, attributes = new { }, @@ -1241,17 +1248,17 @@ public async Task Cannot_update_resource_for_unknown_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'performers' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'performers' with ID '{performerId}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1259,7 +1266,7 @@ public async Task Cannot_update_resource_for_unknown_ID() public async Task Cannot_update_resource_for_incompatible_ID() { // Arrange - string guid = Guid.NewGuid().ToString(); + string guid = Unknown.StringId.Guid; var requestBody = new { @@ -1288,14 +1295,14 @@ public async Task Cannot_update_resource_for_incompatible_ID() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().Be($"Failed to convert '{guid}' of type 'String' to type 'Int32'."); @@ -1337,14 +1344,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); error.Detail.Should().Be("Changing the value of 'createdAt' is not allowed."); @@ -1386,14 +1393,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); error.Detail.Should().Be("Attribute 'isArchived' is read-only."); @@ -1435,17 +1442,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); - error.Detail.Should().BeNull(); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().Be("Resource ID is read-only."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -1474,7 +1481,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingPerformer.StringId, attributes = new { - bornAt = "not-a-valid-time" + bornAt = 123.45 } } } @@ -1484,18 +1491,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'DateTimeOffset'. - Request body:"); - error.Source.Pointer.Should().BeNull(); + error.Detail.Should().Be("Failed to convert attribute 'bornAt' with value '123.45' of type 'Number' to type 'DateTimeOffset'."); + error.Source.Pointer.Should().Be("/atomic:operations[0]"); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs index f65a58f9c3..9487a69551 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateToOneRelationshipTests.cs @@ -595,7 +595,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "lyrics", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -608,14 +608,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().Be("Expected single data element for 'lyric' relationship."); @@ -636,14 +636,14 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() data = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), relationships = new { track = new { data = new { - id = Guid.NewGuid().ToString() + id = Unknown.StringId.For() } } } @@ -655,14 +655,14 @@ public async Task Cannot_create_for_missing_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().Be("Expected 'type' element in 'track' relationship."); @@ -683,15 +683,15 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { lyric = new { data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -703,17 +703,17 @@ public async Task Cannot_create_for_unknown_type_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -731,7 +731,7 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { lyric = new @@ -750,14 +750,14 @@ public async Task Cannot_create_for_missing_ID_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -778,7 +778,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() data = new { type = "musicTracks", - id = Guid.NewGuid().ToString(), + id = Unknown.StringId.For(), relationships = new { lyric = new @@ -786,7 +786,7 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() data = new { type = "lyrics", - id = 99999999, + id = Unknown.StringId.For(), lid = "local-1" } } @@ -799,14 +799,14 @@ public async Task Cannot_create_for_ID_and_local_ID_in_relationship_data() const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' or 'lid' element."); error.Detail.Should().Be("Expected 'id' or 'lid' element in 'lyric' relationship."); @@ -825,6 +825,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string lyricId = Unknown.StringId.For(); + var requestBody = new { atomic__operations = new[] @@ -843,7 +845,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "lyrics", - id = 99999999 + id = lyricId } } } @@ -855,17 +857,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'lyrics' with ID '99999999' in relationship 'lyric' does not exist."); + error.Detail.Should().Be($"Related resource of type 'lyrics' with ID '{lyricId}' in relationship 'lyric' does not exist."); error.Source.Pointer.Should().Be("/atomic:operations[0]"); } @@ -899,7 +901,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "playlists", - id = 99999999 + id = Unknown.StringId.For() } } } @@ -911,14 +913,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/operations"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().Be("Relationship 'lyric' contains incompatible resource type 'playlists'."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs index 2703102568..cce9320cce 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CarExpressionRewriter.cs @@ -23,12 +23,12 @@ internal sealed class CarExpressionRewriter : QueryExpressionRewriter private readonly AttrAttribute _regionIdAttribute; private readonly AttrAttribute _licensePlateAttribute; - public CarExpressionRewriter(IResourceContextProvider resourceContextProvider) + public CarExpressionRewriter(IResourceGraph resourceGraph) { - ResourceContext carResourceContext = resourceContextProvider.GetResourceContext(); + ResourceContext carResourceContext = resourceGraph.GetResourceContext(); - _regionIdAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.RegionId)); - _licensePlateAttribute = carResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Car.LicensePlate)); + _regionIdAttribute = carResourceContext.GetAttributeByPropertyName(nameof(Car.RegionId)); + _licensePlateAttribute = carResourceContext.GetAttributeByPropertyName(nameof(Car.LicensePlate)); } public override QueryExpression VisitComparison(ComparisonExpression expression, object argument) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs index 979bab3c02..1eff765ccd 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys/CompositeKeyTests.cs @@ -60,8 +60,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(car.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); } [Fact] @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/cars/" + car.StringId; + string route = $"/cars/{car.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -89,8 +89,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(car.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(car.StringId); } [Fact] @@ -118,8 +118,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(car.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); } [Fact] @@ -147,8 +147,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(car.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(car.StringId); } [Fact] @@ -234,7 +234,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/engines/" + existingEngine.StringId; + string route = $"/engines/{existingEngine.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -290,7 +290,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/engines/" + existingEngine.StringId; + string route = $"/engines/{existingEngine.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -527,14 +527,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/dealerships/{existingDealership.StringId}/relationships/inventory"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be("Related resource of type 'cars' with ID '999:XX-YY-22' in relationship 'inventory' does not exist."); @@ -557,7 +557,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/cars/" + existingCar.StringId; + string route = $"/cars/{existingCar.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs index 02ec68055d..cca76a2e53 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/AcceptHeaderTests.cs @@ -63,7 +63,7 @@ public async Task Permits_no_Accept_headers_at_operations_endpoint() const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -116,10 +116,10 @@ public async Task Permits_JsonApi_without_parameters_in_Accept_headers() Action setRequestHeaders = headers => { headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; q=0.3")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; profile=some")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; ext=other")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; unknown=unexpected")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; q=0.3")); }; // Act @@ -158,14 +158,14 @@ public async Task Permits_JsonApi_with_AtomicOperations_extension_in_Accept_head Action setRequestHeaders = headers => { headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; profile=some")); headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType)); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + ";ext=\"https://jsonapi.org/ext/atomic\"; q=0.2")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; unknown=unexpected")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType};ext=\"https://jsonapi.org/ext/atomic\"; q=0.2")); }; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -180,24 +180,25 @@ public async Task Denies_JsonApi_with_parameters_in_Accept_headers() Action setRequestHeaders = headers => { headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("text/html")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; profile=some")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; ext=other")); - headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.MediaType + "; unknown=unexpected")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; profile=some")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; ext=other")); + headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"{HeaderConstants.MediaType}; unknown=unexpected")); headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(HeaderConstants.AtomicOperationsMediaType)); }; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route, setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotAcceptable); error.Title.Should().Be("The specified Accept header value does not contain any supported media types."); error.Detail.Should().Be("Please include 'application/vnd.api+json' in the Accept header values."); + error.Source.Header.Should().Be("Accept"); } [Fact] @@ -232,18 +233,19 @@ public async Task Denies_JsonApi_in_Accept_headers_at_operations_endpoint() }; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); + (HttpResponseMessage httpResponse, Document responseDocument) = + await _testContext.ExecutePostAsync(route, requestBody, contentType, setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotAcceptable); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotAcceptable); error.Title.Should().Be("The specified Accept header value does not contain any supported media types."); error.Detail.Should().Be("Please include 'application/vnd.api+json; ext=\"https://jsonapi.org/ext/atomic\"' in the Accept header values."); + error.Source.Header.Should().Be("Accept"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs index 3ba8975183..1ae336c663 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/ContentTypeHeaderTests.cs @@ -88,18 +88,18 @@ public async Task Denies_unknown_ContentType_header() const string contentType = "text/html"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be("Please specify 'application/vnd.api+json' instead of 'text/html' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -123,7 +123,7 @@ public async Task Permits_JsonApi_ContentType_header() // Act // ReSharper disable once RedundantArgumentDefaultValue - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); @@ -156,7 +156,7 @@ public async Task Permits_JsonApi_ContentType_header_with_AtomicOperations_exten const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -179,21 +179,21 @@ public async Task Denies_JsonApi_ContentType_header_with_profile() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; profile=something"; + string contentType = $"{HeaderConstants.MediaType}; profile=something"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -213,21 +213,21 @@ public async Task Denies_JsonApi_ContentType_header_with_extension() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; ext=something"; + string contentType = $"{HeaderConstants.MediaType}; ext=something"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -250,18 +250,18 @@ public async Task Denies_JsonApi_ContentType_header_with_AtomicOperations_extens const string contentType = HeaderConstants.AtomicOperationsMediaType; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -281,21 +281,21 @@ public async Task Denies_JsonApi_ContentType_header_with_CharSet() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; charset=ISO-8859-4"; + string contentType = $"{HeaderConstants.MediaType}; charset=ISO-8859-4"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -315,21 +315,21 @@ public async Task Denies_JsonApi_ContentType_header_with_unknown_parameter() }; const string route = "/policies"; - const string contentType = HeaderConstants.MediaType + "; unknown=unexpected"; + string contentType = $"{HeaderConstants.MediaType}; unknown=unexpected"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be($"Please specify 'application/vnd.api+json' instead of '{contentType}' for the Content-Type header value."); + error.Source.Header.Should().Be("Content-Type"); } [Fact] @@ -360,8 +360,7 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() // Act // ReSharper disable once RedundantArgumentDefaultValue - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePostAsync(route, requestBody, contentType); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody, contentType); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); @@ -370,10 +369,11 @@ public async Task Denies_JsonApi_ContentType_header_at_operations_endpoint() string detail = $"Please specify '{HeaderConstants.AtomicOperationsMediaType}' instead of '{contentType}' for the Content-Type header value."; - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnsupportedMediaType); error.Title.Should().Be("The specified Content-Type header value is not supported."); error.Detail.Should().Be(detail); + error.Source.Header.Should().Be("Content-Type"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs index a467b0ed25..51bb1c6f5c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/ActionResultTests.cs @@ -31,7 +31,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/toothbrushes/" + toothbrush.StringId; + string route = $"/toothbrushes/{toothbrush.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -39,25 +39,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(toothbrush.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(toothbrush.StringId); } [Fact] public async Task Converts_empty_ActionResult_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.EmptyActionResultId; + string route = $"/toothbrushes/{BaseToothbrushesController.EmptyActionResultId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("NotFound"); error.Detail.Should().BeNull(); @@ -67,17 +67,17 @@ public async Task Converts_empty_ActionResult_to_error_collection() public async Task Converts_ActionResult_with_error_object_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithErrorObjectId; + string route = $"/toothbrushes/{BaseToothbrushesController.ActionResultWithErrorObjectId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("No toothbrush with that ID exists."); error.Detail.Should().BeNull(); @@ -87,17 +87,17 @@ public async Task Converts_ActionResult_with_error_object_to_error_collection() public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ActionResultWithStringParameter; + string route = $"/toothbrushes/{BaseToothbrushesController.ActionResultWithStringParameter}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Data being returned must be errors or resources."); @@ -107,17 +107,17 @@ public async Task Cannot_convert_ActionResult_with_string_parameter_to_error_col public async Task Converts_ObjectResult_with_error_object_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorObjectId; + string route = $"/toothbrushes/{BaseToothbrushesController.ObjectResultWithErrorObjectId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadGateway); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadGateway); error.Title.Should().BeNull(); error.Detail.Should().BeNull(); @@ -127,27 +127,27 @@ public async Task Converts_ObjectResult_with_error_object_to_error_collection() public async Task Converts_ObjectResult_with_error_objects_to_error_collection() { // Arrange - string route = "/toothbrushes/" + BaseToothbrushesController.ObjectResultWithErrorCollectionId; + string route = $"/toothbrushes/{BaseToothbrushesController.ObjectResultWithErrorCollectionId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(3); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); error1.Title.Should().BeNull(); error1.Detail.Should().BeNull(); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.Unauthorized); error2.Title.Should().BeNull(); error2.Detail.Should().BeNull(); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.ExpectationFailed); error3.Title.Should().Be("This is not a very great request."); error3.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs index 001ada5fb0..4718323321 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ControllerActionResults/BaseToothbrushesController.cs @@ -32,7 +32,7 @@ public override async Task GetAsync(int id, CancellationToken can if (id == ActionResultWithErrorObjectId) { - return NotFound(new Error(HttpStatusCode.NotFound) + return NotFound(new ErrorObject(HttpStatusCode.NotFound) { Title = "No toothbrush with that ID exists." }); @@ -45,16 +45,16 @@ public override async Task GetAsync(int id, CancellationToken can if (id == ObjectResultWithErrorObjectId) { - return Error(new Error(HttpStatusCode.BadGateway)); + return Error(new ErrorObject(HttpStatusCode.BadGateway)); } if (id == ObjectResultWithErrorCollectionId) { var errors = new[] { - new Error(HttpStatusCode.PreconditionFailed), - new Error(HttpStatusCode.Unauthorized), - new Error(HttpStatusCode.ExpectationFailed) + new ErrorObject(HttpStatusCode.PreconditionFailed), + new ErrorObject(HttpStatusCode.Unauthorized), + new ErrorObject(HttpStatusCode.ExpectationFailed) { Title = "This is not a very great request." } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs index ee3d9e2882..f49c18ad9d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/ApiControllerAttributeTests.cs @@ -26,14 +26,14 @@ public async Task ApiController_attribute_transforms_NotFound_action_result_with const string route = "/world-civilians/missing"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.Links.About.Should().Be("https://tools.ietf.org/html/rfc7231#section-6.5.4"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs index 6565f2074a..05b7c0214d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes/CustomRouteTests.cs @@ -38,7 +38,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/world-api/civilization/popular/towns/" + town.StringId; + string route = $"/world-api/civilization/popular/towns/{town.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -46,16 +46,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("towns"); - responseDocument.SingleData.Id.Should().Be(town.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(town.Name); - responseDocument.SingleData.Attributes["latitude"].Should().Be(town.Latitude); - responseDocument.SingleData.Attributes["longitude"].Should().Be(town.Longitude); - responseDocument.SingleData.Relationships["civilians"].Links.Self.Should().Be(HostPrefix + route + "/relationships/civilians"); - responseDocument.SingleData.Relationships["civilians"].Links.Related.Should().Be(HostPrefix + route + "/civilians"); - responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("towns"); + responseDocument.Data.SingleValue.Id.Should().Be(town.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(town.Name); + responseDocument.Data.SingleValue.Attributes["latitude"].Should().Be(town.Latitude); + responseDocument.Data.SingleValue.Attributes["longitude"].Should().Be(town.Longitude); + responseDocument.Data.SingleValue.Relationships["civilians"].Links.Self.Should().Be($"{HostPrefix}{route}/relationships/civilians"); + responseDocument.Data.SingleValue.Relationships["civilians"].Links.Related.Should().Be($"{HostPrefix}{route}/civilians"); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); } [Fact] @@ -79,10 +79,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(5); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Type == "towns"); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Attributes.Any()); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Relationships.Any()); + responseDocument.Data.ManyValue.Should().HaveCount(5); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "towns"); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Attributes.Any()); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.Any()); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs index 994ce5dbb0..0e0482dbce 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading/EagerLoadingTests.cs @@ -44,7 +44,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/buildings/" + building.StringId; + string route = $"/buildings/{building.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -52,12 +52,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(building.StringId); - responseDocument.SingleData.Attributes["number"].Should().Be(building.Number); - responseDocument.SingleData.Attributes["windowCount"].Should().Be(4); - responseDocument.SingleData.Attributes["primaryDoorColor"].Should().Be(building.PrimaryDoor.Color); - responseDocument.SingleData.Attributes["secondaryDoorColor"].Should().Be(building.SecondaryDoor.Color); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(building.StringId); + responseDocument.Data.SingleValue.Attributes["number"].Should().Be(building.Number); + responseDocument.Data.SingleValue.Attributes["windowCount"].Should().Be(4); + responseDocument.Data.SingleValue.Attributes["primaryDoorColor"].Should().Be(building.PrimaryDoor.Color); + responseDocument.Data.SingleValue.Attributes["secondaryDoorColor"].Should().Be(building.SecondaryDoor.Color); } [Fact] @@ -80,7 +80,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/streets/" + street.StringId; + string route = $"/streets/{street.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -88,12 +88,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(street.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(street.Name); - responseDocument.SingleData.Attributes["buildingCount"].Should().Be(2); - responseDocument.SingleData.Attributes["doorTotalCount"].Should().Be(3); - responseDocument.SingleData.Attributes["windowTotalCount"].Should().Be(5); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(street.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(street.Name); + responseDocument.Data.SingleValue.Attributes["buildingCount"].Should().Be(2); + responseDocument.Data.SingleValue.Attributes["doorTotalCount"].Should().Be(3); + responseDocument.Data.SingleValue.Attributes["windowTotalCount"].Should().Be(5); } [Fact] @@ -119,11 +119,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(street.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["windowTotalCount"].Should().Be(3); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(street.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["windowTotalCount"].Should().Be(3); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -151,9 +151,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(state.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(state.Name); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(state.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(state.Name); responseDocument.Included.Should().HaveCount(2); @@ -194,11 +194,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(state.Cities[0].StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["name"].Should().Be(state.Cities[0].Name); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(state.Cities[0].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["name"].Should().Be(state.Cities[0].Name); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("streets"); @@ -235,13 +235,13 @@ public async Task Can_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["number"].Should().Be(newBuilding.Number); - responseDocument.SingleData.Attributes["windowCount"].Should().Be(0); - responseDocument.SingleData.Attributes["primaryDoorColor"].Should().BeNull(); - responseDocument.SingleData.Attributes["secondaryDoorColor"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["number"].Should().Be(newBuilding.Number); + responseDocument.Data.SingleValue.Attributes["windowCount"].Should().Be(0); + responseDocument.Data.SingleValue.Attributes["primaryDoorColor"].Should().BeNull(); + responseDocument.Data.SingleValue.Attributes["secondaryDoorColor"].Should().BeNull(); - int newBuildingId = int.Parse(responseDocument.SingleData.Id); + int newBuildingId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -297,7 +297,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/buildings/" + existingBuilding.StringId; + string route = $"/buildings/{existingBuilding.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -343,7 +343,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/buildings/" + existingBuilding.StringId; + string route = $"/buildings/{existingBuilding.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs index e728b74230..d6af489f76 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/AlternateExceptionHandler.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Serialization.Objects; @@ -23,12 +24,14 @@ protected override LogLevel GetLogLevel(Exception exception) return base.GetLogLevel(exception); } - protected override ErrorDocument CreateErrorDocument(Exception exception) + protected override Document CreateErrorDocument(Exception exception) { if (exception is ConsumerArticleIsNoLongerAvailableException articleException) { - articleException.Errors[0].Meta.Data.Add("support", - $"Please contact us for info about similar articles at {articleException.SupportEmailAddress}."); + articleException.Errors[0].Meta = new Dictionary + { + ["Support"] = $"Please contact us for info about similar articles at {articleException.SupportEmailAddress}." + }; } return base.CreateErrorDocument(exception); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs index bbf635822d..78260204d7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ConsumerArticleIsNoLongerAvailableException.cs @@ -9,7 +9,7 @@ internal sealed class ConsumerArticleIsNoLongerAvailableException : JsonApiExcep public string SupportEmailAddress { get; } public ConsumerArticleIsNoLongerAvailableException(string articleCode, string supportEmailAddress) - : base(new Error(HttpStatusCode.Gone) + : base(new ErrorObject(HttpStatusCode.Gone) { Title = "The requested article is no longer available.", Detail = $"Article with code '{articleCode}' is no longer available." diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs index adbb207632..ff8a61471f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling/ExceptionHandlerTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -9,7 +10,6 @@ using JsonApiDotNetCore.Serialization.Objects; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; using TestBuildingBlocks; using Xunit; @@ -55,7 +55,7 @@ public async Task Logs_and_produces_error_response_for_custom_exception() var consumerArticle = new ConsumerArticle { - Code = ConsumerArticleService.UnavailableArticlePrefix + "123" + Code = $"{ConsumerArticleService.UnavailableArticlePrefix}123" }; await _testContext.RunOnDatabaseAsync(async dbContext => @@ -64,21 +64,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/consumerArticles/" + consumerArticle.StringId; + string route = $"/consumerArticles/{consumerArticle.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Gone); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Gone); error.Title.Should().Be("The requested article is no longer available."); error.Detail.Should().Be("Article with code 'X123' is no longer available."); - error.Meta.Data["support"].Should().Be("Please contact us for info about similar articles at company@email.com."); + ((JsonElement)error.Meta["support"]).GetString().Should().Be("Please contact us for info about similar articles at company@email.com."); loggerFactory.Logger.Messages.Should().HaveCount(1); loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Warning); @@ -100,22 +100,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/throwingArticles/" + throwingArticle.StringId; + string route = $"/throwingArticles/{throwingArticle.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Exception has been thrown by the target of an invocation."); - IEnumerable stackTraceLines = ((JArray)error.Meta.Data["stackTrace"]).Select(token => token.Value()); + IEnumerable stackTraceLines = ((JsonElement)error.Meta["stackTrace"]).EnumerateArray().Select(token => token.GetString()); stackTraceLines.Should().ContainMatch("* System.InvalidOperationException: Article status could not be determined.*"); loggerFactory.Logger.Messages.Should().HaveCount(1); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs index 517f9b2f1b..02073c4bdf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS/HostingTests.cs @@ -46,27 +46,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string galleryLink = HostPrefix + $"/iis-application-virtual-directory/public-api/artGalleries/{gallery.StringId}"; + string galleryLink = $"{HostPrefix}/iis-application-virtual-directory/public-api/artGalleries/{gallery.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(galleryLink); - responseDocument.ManyData[0].Relationships["paintings"].Links.Self.Should().Be(galleryLink + "/relationships/paintings"); - responseDocument.ManyData[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(galleryLink); + responseDocument.Data.ManyValue[0].Relationships["paintings"].Links.Self.Should().Be($"{galleryLink}/relationships/paintings"); + responseDocument.Data.ManyValue[0].Relationships["paintings"].Links.Related.Should().Be($"{galleryLink}/paintings"); - string paintingLink = HostPrefix + - $"/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{gallery.Paintings.ElementAt(0).StringId}"; + string paintingLink = + $"{HostPrefix}/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{gallery.Paintings.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(paintingLink); - responseDocument.Included[0].Relationships["exposedAt"].Links.Self.Should().Be(paintingLink + "/relationships/exposedAt"); - responseDocument.Included[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); + responseDocument.Included[0].Relationships["exposedAt"].Links.Self.Should().Be($"{paintingLink}/relationships/exposedAt"); + responseDocument.Included[0].Relationships["exposedAt"].Links.Related.Should().Be($"{paintingLink}/exposedAt"); } [Fact] @@ -91,26 +91,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string paintingLink = HostPrefix + $"/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{painting.StringId}"; + string paintingLink = $"{HostPrefix}/iis-application-virtual-directory/custom/path/to/paintings-of-the-world/{painting.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(paintingLink); - responseDocument.ManyData[0].Relationships["exposedAt"].Links.Self.Should().Be(paintingLink + "/relationships/exposedAt"); - responseDocument.ManyData[0].Relationships["exposedAt"].Links.Related.Should().Be(paintingLink + "/exposedAt"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(paintingLink); + responseDocument.Data.ManyValue[0].Relationships["exposedAt"].Links.Self.Should().Be($"{paintingLink}/relationships/exposedAt"); + responseDocument.Data.ManyValue[0].Relationships["exposedAt"].Links.Related.Should().Be($"{paintingLink}/exposedAt"); - string galleryLink = HostPrefix + $"/iis-application-virtual-directory/public-api/artGalleries/{painting.ExposedAt.StringId}"; + string galleryLink = $"{HostPrefix}/iis-application-virtual-directory/public-api/artGalleries/{painting.ExposedAt.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(galleryLink); - responseDocument.Included[0].Relationships["paintings"].Links.Self.Should().Be(galleryLink + "/relationships/paintings"); - responseDocument.Included[0].Relationships["paintings"].Links.Related.Should().Be(galleryLink + "/paintings"); + responseDocument.Included[0].Relationships["paintings"].Links.Self.Should().Be($"{galleryLink}/relationships/paintings"); + responseDocument.Included[0].Relationships["paintings"].Links.Related.Should().Be($"{galleryLink}/paintings"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs index 7d4f85b222..4d7c5b64ea 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/HexadecimalCodec.cs @@ -19,7 +19,7 @@ public int Decode(string value) if (!value.StartsWith("x", StringComparison.Ordinal)) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Invalid ID value.", Detail = $"The value '{value}' is not a valid hexadecimal value." @@ -53,7 +53,7 @@ public string Encode(int value) } string stringValue = value.ToString(); - return 'x' + ToHexString(stringValue); + return $"x{ToHexString(stringValue)}"; } private static string ToHexString(string value) diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs index 73f8da6a5f..8c81bee08b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/IdObfuscationTests.cs @@ -44,8 +44,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); } [Fact] @@ -62,7 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }); var codec = new HexadecimalCodec(); - string route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{codec.Encode(99999999)}')"; + string route = $"/bankAccounts?filter=any(id,'{accounts[1].StringId}','{codec.Encode(Unknown.TypedId.Int32)}')"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -70,8 +70,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); } [Fact] @@ -81,14 +81,14 @@ public async Task Cannot_get_primary_resource_for_invalid_ID() const string route = "/bankAccounts/not-a-hex-value"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Invalid ID value."); error.Detail.Should().Be("The value 'not-a-hex-value' is not a valid hexadecimal value."); @@ -106,7 +106,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/debitCards/" + card.StringId; + string route = $"/debitCards/{card.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -114,8 +114,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(card.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(card.StringId); } [Fact] @@ -139,9 +139,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(account.Cards[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(account.Cards[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(account.Cards[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(account.Cards[1].StringId); } [Fact] @@ -165,8 +165,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(account.Cards[0].StringId); @@ -195,8 +195,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(account.Cards[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(account.Cards[0].StringId); } [Fact] @@ -244,11 +244,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Attributes["ownerName"].Should().Be(newCard.OwnerName); - responseDocument.SingleData.Attributes["pinCode"].Should().Be(newCard.PinCode); + responseDocument.Data.SingleValue.Attributes["ownerName"].Should().Be(newCard.OwnerName); + responseDocument.Data.SingleValue.Attributes["pinCode"].Should().Be(newCard.PinCode); var codec = new HexadecimalCodec(); - int newCardId = codec.Decode(responseDocument.SingleData.Id); + int newCardId = codec.Decode(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -307,7 +307,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/bankAccounts/" + existingAccount.StringId; + string route = $"/bankAccounts/{existingAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -430,7 +430,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/bankAccounts/" + existingAccount.StringId; + string route = $"/bankAccounts/{existingAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -449,23 +449,23 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_delete_missing_resource() + public async Task Cannot_delete_unknown_resource() { // Arrange var codec = new HexadecimalCodec(); - string stringId = codec.Encode(99999999); + string stringId = codec.Encode(Unknown.TypedId.Int32); - string route = "/bankAccounts/" + stringId; + string route = $"/bankAccounts/{stringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'bankAccounts' with ID '{stringId}' does not exist."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs index f3441bf5af..5e819e5ce6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/ModelStateValidationTests.cs @@ -42,14 +42,14 @@ public async Task Cannot_create_resource_with_omitted_required_attribute() const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -76,14 +76,14 @@ public async Task Cannot_create_resource_with_null_for_required_attribute_value( const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -110,14 +110,14 @@ public async Task Cannot_create_resource_with_invalid_attribute_value() const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); @@ -149,9 +149,9 @@ public async Task Can_create_resource_with_valid_attribute_value() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be("Projects"); - responseDocument.SingleData.Attributes["isCaseSensitive"].Should().Be(true); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be("Projects"); + responseDocument.Data.SingleValue.Attributes["isCaseSensitive"].Should().Be(true); } [Fact] @@ -173,26 +173,26 @@ public async Task Cannot_create_resource_with_multiple_violations() const string route = "/systemDirectories"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(3); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The Name field is required."); error1.Source.Pointer.Should().Be("/data/attributes/name"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field SizeInBytes must be between 0 and 9223372036854775807."); error2.Source.Pointer.Should().Be("/data/attributes/sizeInBytes"); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error3.Title.Should().Be("Input validation failed."); error3.Detail.Should().Be("The IsCaseSensitive field is required."); @@ -282,9 +282,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be("Projects"); - responseDocument.SingleData.Attributes["isCaseSensitive"].Should().Be(true); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be("Projects"); + responseDocument.Data.SingleValue.Attributes["isCaseSensitive"].Should().Be(true); } [Fact] @@ -361,7 +361,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -401,17 +401,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The Name field is required."); @@ -447,17 +447,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field Name must match the regular expression '^[\\w\\s]+$'."); @@ -485,7 +485,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "systemDirectories", - id = -1, + id = "-1", attributes = new { name = "Repositories" @@ -499,7 +499,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "systemDirectories", - id = -1 + id = "-1" } } } @@ -510,20 +510,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/systemDirectories/-1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error1.Title.Should().Be("Input validation failed."); error1.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); error1.Source.Pointer.Should().Be("/data/attributes/id"); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error2.Title.Should().Be("Input validation failed."); error2.Detail.Should().Be("The field Id must match the regular expression '^[0-9]+$'."); @@ -559,7 +559,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -670,7 +670,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -729,7 +729,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -783,7 +783,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -830,7 +830,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId + "/relationships/parent"; + string route = $"/systemDirectories/{directory.StringId}/relationships/parent"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -886,7 +886,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId + "/relationships/files"; + string route = $"/systemDirectories/{directory.StringId}/relationships/files"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs index 2be7e452db..bbf49d0b73 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState/NoModelStateValidationTests.cs @@ -32,7 +32,7 @@ public async Task Can_create_resource_with_invalid_attribute_value() attributes = new { name = "!@#$%^&*().-", - isCaseSensitive = "false" + isCaseSensitive = false } } }; @@ -45,8 +45,8 @@ public async Task Can_create_resource_with_invalid_attribute_value() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be("!@#$%^&*().-"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be("!@#$%^&*().-"); } [Fact] @@ -78,7 +78,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/systemDirectories/" + directory.StringId; + string route = $"/systemDirectories/{directory.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs index cbe777c945..912bd9c9ac 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs @@ -71,11 +71,11 @@ private static void AssertHasValidInitialStage(Workflow resource) { if (resource.Stage != WorkflowStage.Created) { - throw new JsonApiException(new Error(HttpStatusCode.UnprocessableEntity) + throw new JsonApiException(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Invalid workflow stage.", Detail = $"Initial stage of workflow must be '{WorkflowStage.Created}'.", - Source = + Source = new ErrorSource { Pointer = "/data/attributes/stage" } @@ -88,11 +88,11 @@ private static void AssertCanTransitionToStage(WorkflowStage fromStage, Workflow { if (!CanTransitionToStage(fromStage, toStage)) { - throw new JsonApiException(new Error(HttpStatusCode.UnprocessableEntity) + throw new JsonApiException(new ErrorObject(HttpStatusCode.UnprocessableEntity) { Title = "Invalid workflow stage.", Detail = $"Cannot transition from '{fromStage}' to '{toStage}'.", - Source = + Source = new ErrorSource { Pointer = "/data/attributes/stage" } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs index 2631aa1d9c..d3cc69efb6 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowTests.cs @@ -50,7 +50,7 @@ public async Task Can_create_in_valid_stage() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); } [Fact] @@ -72,14 +72,14 @@ public async Task Cannot_create_in_invalid_stage() const string route = "/workflows"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Invalid workflow stage."); error.Detail.Should().Be("Initial stage of workflow must be 'Created'."); @@ -114,17 +114,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workflows/" + existingWorkflow.StringId; + string route = $"/workflows/{existingWorkflow.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Invalid workflow stage."); error.Detail.Should().Be("Cannot transition from 'OnHold' to 'Succeeded'."); @@ -159,7 +159,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workflows/" + existingWorkflow.StringId; + string route = $"/workflows/{existingWorkflow.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs index 299a2a8f8d..7f03363f30 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs @@ -49,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/photoAlbums/" + album.StringId; + string route = $"/api/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -57,17 +57,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(HostPrefix + route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(HostPrefix + route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{HostPrefix}{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{HostPrefix}{route}/photos"); } [Fact] @@ -92,26 +92,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/api/photoAlbums/{album.StringId}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/api/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -135,19 +135,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/api/photoAlbums/{photo.Album.StringId}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -171,19 +171,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/api/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -207,16 +207,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/api/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/api/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -240,16 +240,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/api/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/api/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -294,25 +294,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/api/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/api/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -356,26 +356,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/api/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/api/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); - string albumLink = HostPrefix + $"/api/photoAlbums/{existingAlbum.StringId}"; + string albumLink = $"{HostPrefix}/api/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs index 7d0e239c1a..26d5dcc46a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs @@ -49,7 +49,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/photoAlbums/" + album.StringId; + string route = $"/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -57,17 +57,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(HostPrefix + route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(HostPrefix + route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(HostPrefix + route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{HostPrefix}{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{HostPrefix}{route}/photos"); } [Fact] @@ -92,26 +92,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); - responseDocument.Links.Last.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/photoAlbums/{album.StringId}"; + string albumLink = $"{HostPrefix}/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -135,19 +135,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/photoAlbums/{photo.Album.StringId}"; + string albumLink = $"{HostPrefix}/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -171,19 +171,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/photos/{album.Photos.ElementAt(0).StringId}"; + string photoLink = $"{HostPrefix}/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -207,16 +207,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/photos/{photo.StringId}/album"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/photos/{photo.StringId}/album"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -240,16 +240,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.Related.Should().Be(HostPrefix + $"/photoAlbums/{album.StringId}/photos"); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.Related.Should().Be($"{HostPrefix}/photoAlbums/{album.StringId}/photos"); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -294,25 +294,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = HostPrefix + $"/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"{HostPrefix}/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); - string photoLink = HostPrefix + $"/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -356,26 +356,26 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Related.Should().BeNull(); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string photoLink = HostPrefix + $"/photos/{existingPhoto.StringId}"; + string photoLink = $"{HostPrefix}/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); - string albumLink = HostPrefix + $"/photoAlbums/{existingAlbum.StringId}"; + string albumLink = $"{HostPrefix}/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs index d000d18499..e7aee555c3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/LinkInclusionTests.cs @@ -45,11 +45,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Self.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Related.Should().NotBeNull(); - responseDocument.SingleData.Relationships["album"].Links.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Self.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["album"].Links.Should().BeNull(); responseDocument.Included.Should().HaveCount(2); @@ -85,11 +85,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Self.Should().BeNull(); - responseDocument.SingleData.Relationships["photo"].Links.Related.Should().NotBeNull(); - responseDocument.SingleData.Relationships.Should().NotContainKey("album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Self.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["photo"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships.Should().NotContainKey("album"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs index 4bc2e3f465..effa2b0d8c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs @@ -47,7 +47,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/photoAlbums/" + album.StringId; + string route = $"/api/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -62,10 +62,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(route); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{route}/photos"); } [Fact] @@ -99,17 +99,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/api/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/api/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -142,10 +142,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/api/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -178,10 +178,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/api/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -212,9 +212,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -245,9 +245,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -299,18 +299,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = $"/api/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"/api/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/api/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -363,17 +363,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/api/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); string albumLink = $"/api/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs index 5221df597b..6256adf66d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs @@ -47,7 +47,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/photoAlbums/" + album.StringId; + string route = $"/photoAlbums/{album.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -62,10 +62,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(route); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(route + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(route + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(route); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{route}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{route}/photos"); } [Fact] @@ -99,17 +99,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/photoAlbums/{album.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(albumLink); - responseDocument.ManyData[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.ManyData[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(albumLink); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.ManyValue[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/photos/{album.Photos.ElementAt(0).StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -142,10 +142,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string albumLink = $"/photoAlbums/{photo.Album.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } [Fact] @@ -178,10 +178,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/photos/{album.Photos.ElementAt(0).StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(photoLink); - responseDocument.ManyData[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.ManyData[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(photoLink); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.ManyValue[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -212,9 +212,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -245,9 +245,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); } [Fact] @@ -299,18 +299,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); - string albumLink = $"/photoAlbums/{responseDocument.SingleData.Id}"; + string albumLink = $"/photoAlbums/{responseDocument.Data.SingleValue.Id}"; - responseDocument.SingleData.Links.Self.Should().Be(albumLink); - responseDocument.SingleData.Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.SingleData.Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Data.SingleValue.Links.Self.Should().Be(albumLink); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Data.SingleValue.Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); string photoLink = $"/photos/{existingPhoto.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(photoLink); - responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Included[0].Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Included[0].Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); } [Fact] @@ -363,17 +363,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string photoLink = $"/photos/{existingPhoto.StringId}"; - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be(photoLink); - responseDocument.SingleData.Relationships["album"].Links.Self.Should().Be(photoLink + "/relationships/album"); - responseDocument.SingleData.Relationships["album"].Links.Related.Should().Be(photoLink + "/album"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be(photoLink); + responseDocument.Data.SingleValue.Relationships["album"].Links.Self.Should().Be($"{photoLink}/relationships/album"); + responseDocument.Data.SingleValue.Relationships["album"].Links.Related.Should().Be($"{photoLink}/album"); string albumLink = $"/photoAlbums/{existingAlbum.StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(albumLink); - responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be(albumLink + "/relationships/photos"); - responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be(albumLink + "/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Self.Should().Be($"{albumLink}/relationships/photos"); + responseDocument.Included[0].Relationships["photos"].Links.Related.Should().Be($"{albumLink}/photos"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs index 3c6f386ed7..fbd9892f9a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResourceMetaTests.cs @@ -40,8 +40,8 @@ public async Task Returns_resource_meta_from_ResourceDefinition() var hitCounter = _testContext.Factory.Services.GetRequiredService(); List tickets = _fakers.SupportTicket.Generate(3); - tickets[0].Description = "Critical: " + tickets[0].Description; - tickets[2].Description = "Critical: " + tickets[2].Description; + tickets[0].Description = $"Critical: {tickets[0].Description}"; + tickets[2].Description = $"Critical: {tickets[2].Description}"; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -58,10 +58,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Meta.Should().ContainKey("hasHighPriority"); - responseDocument.ManyData[1].Meta.Should().BeNull(); - responseDocument.ManyData[2].Meta.Should().ContainKey("hasHighPriority"); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Meta.Should().ContainKey("hasHighPriority"); + responseDocument.Data.ManyValue[1].Meta.Should().BeNull(); + responseDocument.Data.ManyValue[2].Meta.Should().ContainKey("hasHighPriority"); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -79,7 +79,7 @@ public async Task Returns_resource_meta_from_ResourceDefinition_in_included_reso ProductFamily family = _fakers.ProductFamily.Generate(); family.Tickets = _fakers.SupportTicket.Generate(1); - family.Tickets[0].Description = "Critical: " + family.Tickets[0].Description; + family.Tickets[0].Description = $"Critical: {family.Tickets[0].Description}"; await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -96,7 +96,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Meta.Should().ContainKey("hasHighPriority"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs index 15297d7b37..0c698644da 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/ResponseMetaTests.cs @@ -48,6 +48,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Should().BeJson(@"{ + ""links"": { + ""self"": ""http://localhost/supportTickets"", + ""first"": ""http://localhost/supportTickets"" + }, + ""data"": [], ""meta"": { ""license"": ""MIT"", ""projectUrl"": ""https://github.com/json-api-dotnet/JsonApiDotNetCore/"", @@ -57,12 +62,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""v2.5.2"", ""v1.3.1"" ] - }, - ""links"": { - ""self"": ""http://localhost/supportTickets"", - ""first"": ""http://localhost/supportTickets"" - }, - ""data"": [] + } }"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs index 7041d5b6e5..a449e1799b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Meta/TopLevelCountTests.cs @@ -1,5 +1,6 @@ using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -54,7 +55,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta["totalResources"].Should().Be(1); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(1); } [Fact] @@ -75,7 +76,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Meta.Should().NotBeNull(); - responseDocument.Meta["totalResources"].Should().Be(0); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(0); } [Fact] @@ -134,7 +135,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/supportTickets/" + existingTicket.StringId; + string route = $"/supportTickets/{existingTicket.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs index be56f0e5fa..bf762c857b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.Group.cs @@ -43,8 +43,8 @@ public async Task Create_group_sends_messages() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -55,7 +55,7 @@ public async Task Create_group_sends_messages() messageBroker.SentMessages.Should().HaveCount(1); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content = messageBroker.SentMessages[0].GetContentAs(); content.GroupId.Should().Be(newGroupId); @@ -121,8 +121,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -134,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => messageBroker.SentMessages.Should().HaveCount(3); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content1 = messageBroker.SentMessages[0].GetContentAs(); content1.GroupId.Should().Be(newGroupId); @@ -180,7 +180,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -264,7 +264,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -351,7 +351,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs index fa591caa2b..fe40df6188 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.User.cs @@ -44,9 +44,9 @@ public async Task Create_user_sends_messages() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().Be(newDisplayName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(newDisplayName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -57,7 +57,7 @@ public async Task Create_user_sends_messages() messageBroker.SentMessages.Should().HaveCount(1); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content = messageBroker.SentMessages[0].GetContentAs(); content.UserId.Should().Be(newUserId); @@ -113,9 +113,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -127,7 +127,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => messageBroker.SentMessages.Should().HaveCount(2); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); var content1 = messageBroker.SentMessages[0].GetContentAs(); content1.UserId.Should().Be(newUserId); @@ -171,7 +171,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -239,7 +239,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -311,7 +311,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -385,7 +385,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -431,7 +431,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -469,7 +469,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs index a8a2c5a373..391ff96781 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/FireForgetTests.cs @@ -46,22 +46,22 @@ public async Task Does_not_send_message_on_write_error() var hitCounter = _testContext.Factory.Services.GetRequiredService(); var messageBroker = _testContext.Factory.Services.GetRequiredService(); - string missingUserId = Guid.NewGuid().ToString(); + string unknownUserId = Unknown.StringId.For(); - string route = "/domainUsers/" + missingUserId; + string route = $"/domainUsers/{unknownUserId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be($"Resource of type 'domainUsers' with ID '{missingUserId}' does not exist."); + error.Detail.Should().Be($"Resource of type 'domainUsers' with ID '{unknownUserId}' does not exist."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -89,17 +89,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.ServiceUnavailable); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.ServiceUnavailable); error.Title.Should().Be("Message delivery failed."); error.Detail.Should().BeNull(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs index ee8b2fae7f..1fbf64df46 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/FireAndForgetDelivery/MessageBroker.cs @@ -27,7 +27,7 @@ internal Task PostMessageAsync(OutgoingMessage message, CancellationToken cancel if (SimulateFailure) { - throw new JsonApiException(new Error(HttpStatusCode.ServiceUnavailable) + throw new JsonApiException(new ErrorObject(HttpStatusCode.ServiceUnavailable) { Title = "Message delivery failed." }); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs index f1ed19ef3e..fccf23a8ef 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/Messages/OutgoingMessage.cs @@ -1,5 +1,5 @@ +using System.Text.Json; using JetBrains.Annotations; -using Newtonsoft.Json; namespace JsonApiDotNetCoreTests.IntegrationTests.Microservices.Messages { @@ -15,9 +15,9 @@ public T GetContentAs() where T : IMessageContent { string namespacePrefix = typeof(IMessageContent).Namespace; - var contentType = System.Type.GetType(namespacePrefix + "." + Type, true); + var contentType = System.Type.GetType($"{namespacePrefix}.{Type}", true); - return (T)JsonConvert.DeserializeObject(Content, contentType); + return (T)JsonSerializer.Deserialize(Content, contentType); } public static OutgoingMessage CreateFromContent(IMessageContent content) @@ -26,7 +26,7 @@ public static OutgoingMessage CreateFromContent(IMessageContent content) { Type = content.GetType().Name, FormatVersion = content.FormatVersion, - Content = JsonConvert.SerializeObject(content) + Content = JsonSerializer.Serialize(content, content.GetType()) }; } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs index 5867e2d962..eda25094b2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.Group.cs @@ -49,8 +49,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -58,7 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -130,8 +130,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroupName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroupName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -140,7 +140,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainGroup), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -192,7 +192,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -279,7 +279,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -331,7 +331,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -372,7 +372,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainGroups/" + existingGroup.StringId; + string route = $"/domainGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs index 2a5d518b66..219da42053 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.User.cs @@ -51,9 +51,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().Be(newDisplayName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(newDisplayName); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -61,7 +61,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -123,9 +123,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["loginName"].Should().Be(newLoginName); - responseDocument.SingleData.Attributes["displayName"].Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["loginName"].Should().Be(newLoginName); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -134,7 +134,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => (typeof(DomainUser), ResourceDefinitionHitCounter.ExtensibilityPoint.OnWritingAsync) }, options => options.WithStrictOrdering()); - Guid newUserId = Guid.Parse(responseDocument.SingleData.Id); + Guid newUserId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -184,7 +184,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -255,7 +255,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -330,7 +330,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -407,7 +407,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -456,7 +456,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -497,7 +497,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/domainUsers/" + existingUser.StringId; + string route = $"/domainUsers/{existingUser.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs index 8bfb695d55..68af373b9d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices/TransactionalOutboxPattern/OutboxTests.cs @@ -51,7 +51,7 @@ public async Task Does_not_add_to_outbox_on_write_error() DomainUser existingUser = _fakers.DomainUser.Generate(); - string missingUserId = Guid.NewGuid().ToString(); + string unknownUserId = Unknown.StringId.For(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -72,7 +72,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "domainUsers", - id = missingUserId + id = unknownUserId } } }; @@ -80,17 +80,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/domainGroups/{existingGroup.StringId}/relationships/users"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be($"Related resource of type 'domainUsers' with ID '{missingUserId}' in relationship 'users' does not exist."); + error.Detail.Should().Be($"Related resource of type 'domainUsers' with ID '{unknownUserId}' in relationship 'users' does not exist."); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs index 9faebf948c..11c4af8983 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy/MultiTenancyTests.cs @@ -68,8 +68,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(shops[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); } [Fact] @@ -98,8 +98,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(shops[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); } [Fact] @@ -128,9 +128,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("webShops"); - responseDocument.ManyData[0].Id.Should().Be(shops[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("webShops"); + responseDocument.Data.ManyValue[0].Id.Should().Be(shops[1].StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webProducts"); @@ -150,17 +150,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/nld/shops/" + shop.StringId; + string route = $"/nld/shops/{shop.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{shop.StringId}' does not exist."); @@ -183,14 +183,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{shop.StringId}/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{shop.StringId}' does not exist."); @@ -213,14 +213,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{product.StringId}/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{product.StringId}' does not exist."); @@ -243,14 +243,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{shop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{shop.StringId}' does not exist."); @@ -273,14 +273,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{product.StringId}/relationships/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{product.StringId}' does not exist."); @@ -312,11 +312,11 @@ public async Task Can_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["url"].Should().Be(newShopUrl); - responseDocument.SingleData.Relationships.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["url"].Should().Be(newShopUrl); + responseDocument.Data.SingleValue.Relationships.Should().NotBeNull(); - int newShopId = int.Parse(responseDocument.SingleData.Id); + int newShopId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -372,14 +372,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/nld/shops"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -426,14 +426,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/nld/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webShops' with ID '{existingShop.StringId}' in relationship 'shop' does not exist."); @@ -468,7 +468,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -516,17 +516,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{existingProduct.StringId}' does not exist."); @@ -572,17 +572,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/shops/" + existingShop.StringId; + string route = $"/nld/shops/{existingShop.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -625,17 +625,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webShops' with ID '{existingShop.StringId}' in relationship 'shop' does not exist."); @@ -663,14 +663,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{existingShop.StringId}' does not exist."); @@ -708,14 +708,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -743,14 +743,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{existingProduct.StringId}/relationships/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{existingProduct.StringId}' does not exist."); @@ -785,14 +785,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/products/{existingProduct.StringId}/relationships/shop"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webShops' with ID '{existingShop.StringId}' in relationship 'shop' does not exist."); @@ -830,14 +830,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{existingShop.StringId}' does not exist."); @@ -874,14 +874,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'webProducts' with ID '{existingProduct.StringId}' in relationship 'products' does not exist."); @@ -916,14 +916,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/nld/shops/{existingShop.StringId}/relationships/products"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webShops' with ID '{existingShop.StringId}' does not exist."); @@ -943,7 +943,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -975,17 +975,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/nld/products/" + existingProduct.StringId; + string route = $"/nld/products/{existingProduct.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'webProducts' with ID '{existingProduct.StringId}' does not exist."); @@ -1023,17 +1023,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string shopLink = $"/nld/shops/{shop.StringId}"; - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Links.Self.Should().Be(shopLink); - responseDocument.ManyData[0].Relationships["products"].Links.Self.Should().Be(shopLink + "/relationships/products"); - responseDocument.ManyData[0].Relationships["products"].Links.Related.Should().Be(shopLink + "/products"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be(shopLink); + responseDocument.Data.ManyValue[0].Relationships["products"].Links.Self.Should().Be($"{shopLink}/relationships/products"); + responseDocument.Data.ManyValue[0].Relationships["products"].Links.Related.Should().Be($"{shopLink}/products"); string productLink = $"/nld/products/{shop.Products[0].StringId}"; responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Links.Self.Should().Be(productLink); - responseDocument.Included[0].Relationships["shop"].Links.Self.Should().Be(productLink + "/relationships/shop"); - responseDocument.Included[0].Relationships["shop"].Links.Related.Should().Be(productLink + "/shop"); + responseDocument.Included[0].Relationships["shop"].Links.Self.Should().Be($"{productLink}/relationships/shop"); + responseDocument.Included[0].Relationships["shop"].Links.Related.Should().Be($"{productLink}/shop"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs new file mode 100644 index 0000000000..1c50a67fb8 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/JsonKebabCaseNamingPolicy.cs @@ -0,0 +1,83 @@ +using System; +using System.Text; +using System.Text.Json; + +namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions +{ + // Based on https://github.com/J0rgeSerran0/JsonNamingPolicy + internal sealed class JsonKebabCaseNamingPolicy : JsonNamingPolicy + { + private const char Separator = '-'; + + public static readonly JsonKebabCaseNamingPolicy Instance = new(); + + public override string ConvertName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + ReadOnlySpan spanName = name.Trim(); + + var stringBuilder = new StringBuilder(); + bool addCharacter = true; + + bool isNextLower = false; + bool isNextUpper = false; + bool isNextSpace = false; + + for (int position = 0; position < spanName.Length; position++) + { + if (position != 0) + { + bool isCurrentSpace = spanName[position] == 32; + bool isPreviousSpace = spanName[position - 1] == 32; + bool isPreviousSeparator = spanName[position - 1] == 95; + + if (position + 1 != spanName.Length) + { + isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123; + isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91; + isNextSpace = spanName[position + 1] == 32; + } + + if (isCurrentSpace && (isPreviousSpace || isPreviousSeparator || isNextUpper || isNextSpace)) + { + addCharacter = false; + } + else + { + bool isCurrentUpper = spanName[position] > 64 && spanName[position] < 91; + bool isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; + bool isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; + + if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace)) + { + stringBuilder.Append(Separator); + } + else + { + if (isCurrentSpace) + { + stringBuilder.Append(Separator); + addCharacter = false; + } + } + } + } + + if (addCharacter) + { + stringBuilder.Append(spanName[position]); + } + else + { + addCharacter = true; + } + } + + return stringBuilder.ToString().ToLower(); + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs index fb5241d17e..952ffd7a14 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingConventionStartup.cs @@ -1,7 +1,6 @@ using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Serialization; using TestBuildingBlocks; namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions @@ -19,10 +18,8 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.IncludeTotalResourceCount = true; options.ValidateModelState = true; - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new KebabCaseNamingStrategy() - }; + options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs index e960c607a5..de91ad49fb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/KebabCasingTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Serialization.Objects; @@ -44,11 +45,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Type == "swimming-pools"); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Attributes.ContainsKey("is-indoor")); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("water-slides")); - responseDocument.ManyData.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("diving-boards")); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "swimming-pools"); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Attributes.ContainsKey("is-indoor")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("water-slides")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("diving-boards")); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("diving-boards"); @@ -57,7 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Relationships.Should().BeNull(); responseDocument.Included[0].Links.Self.Should().Be($"/public-api/diving-boards/{pools[1].DivingBoards[0].StringId}"); - responseDocument.Meta["total-resources"].Should().Be(2); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(2); } [Fact] @@ -84,10 +85,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("water-slides"); - responseDocument.ManyData[0].Id.Should().Be(pool.WaterSlides[1].StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("water-slides"); + responseDocument.Data.ManyValue[0].Id.Should().Be(pool.WaterSlides[1].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); } [Fact] @@ -116,18 +117,18 @@ public async Task Can_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("swimming-pools"); - responseDocument.SingleData.Attributes["is-indoor"].Should().Be(newPool.IsIndoor); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("swimming-pools"); + responseDocument.Data.SingleValue.Attributes["is-indoor"].Should().Be(newPool.IsIndoor); - int newPoolId = int.Parse(responseDocument.SingleData.Id); - string poolLink = route + $"/{newPoolId}"; + int newPoolId = int.Parse(responseDocument.Data.SingleValue.Id); + string poolLink = $"{route}/{newPoolId}"; - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships["water-slides"].Links.Self.Should().Be(poolLink + "/relationships/water-slides"); - responseDocument.SingleData.Relationships["water-slides"].Links.Related.Should().Be(poolLink + "/water-slides"); - responseDocument.SingleData.Relationships["diving-boards"].Links.Self.Should().Be(poolLink + "/relationships/diving-boards"); - responseDocument.SingleData.Relationships["diving-boards"].Links.Related.Should().Be(poolLink + "/diving-boards"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships["water-slides"].Links.Self.Should().Be($"{poolLink}/relationships/water-slides"); + responseDocument.Data.SingleValue.Relationships["water-slides"].Links.Related.Should().Be($"{poolLink}/water-slides"); + responseDocument.Data.SingleValue.Relationships["diving-boards"].Links.Self.Should().Be($"{poolLink}/relationships/diving-boards"); + responseDocument.Data.SingleValue.Relationships["diving-boards"].Links.Related.Should().Be($"{poolLink}/diving-boards"); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -146,17 +147,17 @@ public async Task Applies_casing_convention_on_error_stack_trace() const string route = "/public-api/swimming-pools"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Meta.Data.Should().ContainKey("stack-trace"); + error.Meta.Should().ContainKey("stack-trace"); } [Fact] @@ -184,17 +185,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/public-api/diving-boards/" + existingBoard.StringId; + string route = $"/public-api/diving-boards/{existingBoard.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Input validation failed."); error.Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs new file mode 100644 index 0000000000..65deafe3bc --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingConventionStartup.cs @@ -0,0 +1,25 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + public sealed class PascalCasingConventionStartup : TestableStartup + where TDbContext : DbContext + { + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.Namespace = "PublicApi"; + options.UseRelativeLinks = true; + options.IncludeTotalResourceCount = true; + options.ValidateModelState = true; + + options.SerializerOptions.PropertyNamingPolicy = null; + options.SerializerOptions.DictionaryKeyPolicy = null; + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs new file mode 100644 index 0000000000..d1c511bda9 --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/NamingConventions/PascalCasingTests.cs @@ -0,0 +1,205 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using FluentAssertions; +using JsonApiDotNetCore.Serialization.Objects; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.NamingConventions +{ + public sealed class PascalCasingTests : IClassFixture, SwimmingDbContext>> + { + private readonly IntegrationTestContext, SwimmingDbContext> _testContext; + private readonly SwimmingFakers _fakers = new(); + + public PascalCasingTests(IntegrationTestContext, SwimmingDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.UseController(); + } + + [Fact] + public async Task Can_get_resources_with_include() + { + // Arrange + List pools = _fakers.SwimmingPool.Generate(2); + pools[1].DivingBoards = _fakers.DivingBoard.Generate(1); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + dbContext.SwimmingPools.AddRange(pools); + await dbContext.SaveChangesAsync(); + }); + + const string route = "/PublicApi/SwimmingPools?include=DivingBoards"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Type == "SwimmingPools"); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Attributes.ContainsKey("IsIndoor")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("WaterSlides")); + responseDocument.Data.ManyValue.Should().OnlyContain(resourceObject => resourceObject.Relationships.ContainsKey("DivingBoards")); + + responseDocument.Included.Should().HaveCount(1); + responseDocument.Included[0].Type.Should().Be("DivingBoards"); + responseDocument.Included[0].Id.Should().Be(pools[1].DivingBoards[0].StringId); + responseDocument.Included[0].Attributes["HeightInMeters"].As().Should().BeApproximately(pools[1].DivingBoards[0].HeightInMeters); + responseDocument.Included[0].Relationships.Should().BeNull(); + responseDocument.Included[0].Links.Self.Should().Be($"/PublicApi/DivingBoards/{pools[1].DivingBoards[0].StringId}"); + + ((JsonElement)responseDocument.Meta["Total"]).GetInt32().Should().Be(2); + } + + [Fact] + public async Task Can_filter_secondary_resources_with_sparse_fieldset() + { + // Arrange + SwimmingPool pool = _fakers.SwimmingPool.Generate(); + pool.WaterSlides = _fakers.WaterSlide.Generate(2); + pool.WaterSlides[0].LengthInMeters = 1; + pool.WaterSlides[1].LengthInMeters = 5; + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.SwimmingPools.Add(pool); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/PublicApi/SwimmingPools/{pool.StringId}/WaterSlides" + + "?filter=greaterThan(LengthInMeters,'1')&fields[WaterSlides]=LengthInMeters"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("WaterSlides"); + responseDocument.Data.ManyValue[0].Id.Should().Be(pool.WaterSlides[1].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + } + + [Fact] + public async Task Can_create_resource() + { + // Arrange + SwimmingPool newPool = _fakers.SwimmingPool.Generate(); + + var requestBody = new + { + data = new + { + type = "SwimmingPools", + attributes = new Dictionary + { + ["IsIndoor"] = newPool.IsIndoor + } + } + }; + + const string route = "/PublicApi/SwimmingPools"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("SwimmingPools"); + responseDocument.Data.SingleValue.Attributes["IsIndoor"].Should().Be(newPool.IsIndoor); + + int newPoolId = int.Parse(responseDocument.Data.SingleValue.Id); + string poolLink = $"{route}/{newPoolId}"; + + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships["WaterSlides"].Links.Self.Should().Be($"{poolLink}/relationships/WaterSlides"); + responseDocument.Data.SingleValue.Relationships["WaterSlides"].Links.Related.Should().Be($"{poolLink}/WaterSlides"); + responseDocument.Data.SingleValue.Relationships["DivingBoards"].Links.Self.Should().Be($"{poolLink}/relationships/DivingBoards"); + responseDocument.Data.SingleValue.Relationships["DivingBoards"].Links.Related.Should().Be($"{poolLink}/DivingBoards"); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + SwimmingPool poolInDatabase = await dbContext.SwimmingPools.FirstWithIdAsync(newPoolId); + + poolInDatabase.IsIndoor.Should().Be(newPool.IsIndoor); + }); + } + + [Fact] + public async Task Applies_casing_convention_on_error_stack_trace() + { + // Arrange + const string requestBody = "{ \"data\": {"; + + const string route = "/PublicApi/SwimmingPools"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Meta.Should().ContainKey("StackTrace"); + } + + [Fact] + public async Task Applies_casing_convention_on_source_pointer_from_ModelState() + { + // Arrange + DivingBoard existingBoard = _fakers.DivingBoard.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.DivingBoards.Add(existingBoard); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "DivingBoards", + id = existingBoard.StringId, + attributes = new Dictionary + { + ["HeightInMeters"] = -1 + } + } + }; + + string route = $"/PublicApi/DivingBoards/{existingBoard.StringId}"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Input validation failed."); + error.Detail.Should().Be("The field HeightInMeters must be between 1 and 20."); + error.Source.Pointer.Should().Be("/data/attributes/HeightInMeters"); + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs index 5ca3125da3..90ffe9d9b3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Calendar.cs @@ -11,6 +11,9 @@ public sealed class Calendar : Identifiable [Attr] public string TimeZone { get; set; } + [Attr] + public bool ShowWeekNumbers { get; set; } + [Attr] public int DefaultAppointmentDurationInMinutes { get; set; } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs index df1677f380..f021dd27e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDataTypeTests.cs @@ -1,7 +1,9 @@ using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; +using System.Text.Json.Serialization; using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Common; @@ -27,6 +29,12 @@ public FilterDataTypeTests(IntegrationTestContext(); options.EnableLegacyFilterNotation = false; + + if (!options.SerializerOptions.Converters.Any(converter => converter is JsonStringEnumMemberConverter)) + { + options.SerializerOptions.Converters.Add(new JsonStringEnumMemberConverter()); + options.SerializerOptions.Converters.Add(new JsonTimeSpanConverter()); + } } [Theory] @@ -64,8 +72,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes[attributeName].Should().Be(value is Enum ? value.ToString() : value); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes[attributeName].Should().Be(value); } [Fact] @@ -92,8 +100,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDecimal"].Should().Be(resource.SomeDecimal); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDecimal"].Should().Be(resource.SomeDecimal); } [Fact] @@ -120,8 +128,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someGuid"].Should().Be(resource.SomeGuid.ToString()); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someGuid"].Should().Be(resource.SomeGuid); } [Fact] @@ -148,8 +156,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTime"].Should().BeCloseTo(resource.SomeDateTime); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDateTime"].As().Should().BeCloseTo(resource.SomeDateTime); } [Fact] @@ -176,8 +184,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTimeOffset"].Should().BeCloseTo(resource.SomeDateTimeOffset); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDateTimeOffset"].As().Should().BeCloseTo(resource.SomeDateTimeOffset); } [Fact] @@ -204,8 +212,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someTimeSpan"].Should().Be(resource.SomeTimeSpan.ToString()); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someTimeSpan"].Should().Be(resource.SomeTimeSpan); } [Fact] @@ -227,18 +235,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/filterableResources?filter=equals(someInt32,'ABC')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Query creation failed due to incompatible types."); error.Detail.Should().Be("Failed to convert 'ABC' of type 'String' to type 'Int32'."); - error.Source.Parameter.Should().BeNull(); + error.Source.Should().BeNull(); } [Theory] @@ -291,8 +299,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes[attributeName].Should().Be(null); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes[attributeName].Should().Be(null); } [Theory] @@ -341,8 +349,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes[attributeName].Should().NotBe(null); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes[attributeName].Should().NotBe(null); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs index db03f0517e..4d13fd69c1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterDepthTests.cs @@ -55,8 +55,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); } [Fact] @@ -74,14 +74,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}?filter=equals(caption,'Two')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -111,8 +111,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); } [Fact] @@ -130,14 +130,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}/author?filter=equals(displayName,'John Smith')"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -169,9 +169,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData.Should().ContainSingle(post => post.Id == posts[1].StringId); - responseDocument.ManyData.Should().ContainSingle(post => post.Id == posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().ContainSingle(post => post.Id == posts[1].StringId); + responseDocument.Data.ManyValue.Should().ContainSingle(post => post.Id == posts[2].StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(posts[1].Author.StringId); @@ -199,8 +199,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); } [Fact] @@ -228,8 +228,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); } [Fact] @@ -254,8 +254,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); } [Fact] @@ -285,8 +285,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); } [Fact] @@ -313,7 +313,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Posts[1].StringId); @@ -343,7 +343,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); @@ -381,7 +381,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(posts[1].Labels.First().StringId); @@ -412,7 +412,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); + responseDocument.Data.ManyValue.Should().HaveCount(1); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); @@ -443,9 +443,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[2].StringId); } [Fact] @@ -484,9 +484,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[1].StringId); } [Fact] @@ -526,8 +526,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs index 31b6aa41cd..842a26b5f0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterOperatorTests.cs @@ -53,8 +53,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someString"].Should().Be(resource.SomeString); } [Fact] @@ -88,9 +88,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["otherInt32"].Should().Be(resource.OtherInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["otherInt32"].Should().Be(resource.OtherInt32); } [Fact] @@ -124,9 +124,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); - responseDocument.ManyData[0].Attributes["otherNullableInt32"].Should().Be(resource.OtherNullableInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); + responseDocument.Data.ManyValue[0].Attributes["otherNullableInt32"].Should().Be(resource.OtherNullableInt32); } [Fact] @@ -160,9 +160,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); } [Fact] @@ -196,9 +196,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["someNullableInt32"].Should().Be(resource.SomeNullableInt32); } [Fact] @@ -232,9 +232,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); - responseDocument.ManyData[0].Attributes["someUnsignedInt64"].Should().Be(resource.SomeUnsignedInt64); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue[0].Attributes["someUnsignedInt64"].Should().Be(resource.SomeUnsignedInt64); } [Fact] @@ -244,18 +244,18 @@ public async Task Cannot_filter_equality_on_two_attributes_of_incompatible_types const string route = "/filterableResources?filter=equals(someDouble,someTimeSpan)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Query creation failed due to incompatible types."); error.Detail.Should().Be("No coercion operator is defined between types 'System.TimeSpan' and 'System.Double'."); - error.Source.Parameter.Should().BeNull(); + error.Source.Should().BeNull(); } [Theory] @@ -295,8 +295,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someInt32"].Should().Be(resource.SomeInt32); } [Theory] @@ -337,8 +337,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDouble"].Should().Be(resource.SomeDouble); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDouble"].Should().Be(resource.SomeDouble); } [Theory] @@ -380,8 +380,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someDateTime"].Should().BeCloseTo(resource.SomeDateTime); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someDateTime"].As().Should().BeCloseTo(resource.SomeDateTime); } [Theory] @@ -418,8 +418,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someString"].Should().Be(resource.SomeString); } [Theory] @@ -453,8 +453,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["someString"].Should().Be(resource.SomeString); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["someString"].Should().Be(resource.SomeString); } [Fact] @@ -484,8 +484,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resource.StringId); } [Fact] @@ -531,8 +531,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resources[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resources[1].StringId); } [Fact] @@ -563,8 +563,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resource.StringId); } [Theory] @@ -604,8 +604,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(resource1.StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(resource1.StringId); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs index e4e5fdeec5..8aa640c530 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Filtering/FilterTests.cs @@ -30,42 +30,42 @@ public FilterTests(IntegrationTestContext, public async Task Cannot_filter_in_unknown_scope() { // Arrange - const string route = "/webAccounts?filter[doesNotExist]=equals(title,null)"; + string route = $"/webAccounts?filter[{Unknown.Relationship}]=equals(title,null)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - error.Source.Parameter.Should().Be("filter[doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be($"filter[{Unknown.Relationship}]"); } [Fact] public async Task Cannot_filter_in_unknown_nested_scope() { // Arrange - const string route = "/webAccounts?filter[posts.doesNotExist]=equals(title,null)"; + string route = $"/webAccounts?filter[posts.{Unknown.Relationship}]=equals(title,null)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified filter is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - error.Source.Parameter.Should().Be("filter[posts.doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be($"filter[posts.{Unknown.Relationship}]"); } [Fact] @@ -75,14 +75,14 @@ public async Task Cannot_filter_on_attribute_with_blocked_capability() const string route = "/webAccounts?filter=equals(dateOfBirth,null)"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Filtering on the requested attribute is not allowed."); error.Detail.Should().Be("Filtering on attribute 'dateOfBirth' is not allowed."); @@ -110,9 +110,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(accounts[0].StringId); - responseDocument.ManyData[0].Attributes["userName"].Should().Be(accounts[0].UserName); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes["userName"].Should().Be(accounts[0].UserName); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs index 39bd0e68b5..7236e285eb 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Includes/IncludeTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -52,9 +53,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -83,9 +84,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -115,9 +116,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.Owner.StringId); - responseDocument.SingleData.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("blogPosts"); @@ -147,9 +148,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -179,9 +180,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.StringId); - responseDocument.SingleData.Attributes["text"].Should().Be(comment.Text); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.StringId); + responseDocument.Data.SingleValue.Attributes["text"].Should().Be(comment.Text); responseDocument.Included.Should().HaveCount(2); @@ -215,14 +216,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("comments"); responseDocument.Included[0].Id.Should().Be(post.Comments.Single().StringId); - responseDocument.Included[0].Attributes["createdAt"].Should().BeCloseTo(post.Comments.Single().CreatedAt); + responseDocument.Included[0].Attributes["createdAt"].As().Should().BeCloseTo(post.Comments.Single().CreatedAt); } [Fact] @@ -246,9 +247,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("labels"); @@ -277,10 +278,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("labels"); - responseDocument.ManyData[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); - responseDocument.ManyData[0].Attributes["name"].Should().Be(post.Labels.Single().Name); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("labels"); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); + responseDocument.Data.ManyValue[0].Attributes["name"].Should().Be(post.Labels.Single().Name); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("blogPosts"); @@ -311,9 +312,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.StringId); - responseDocument.SingleData.Attributes["text"].Should().Be(comment.Text); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.StringId); + responseDocument.Data.SingleValue.Attributes["text"].Should().Be(comment.Text); responseDocument.Included.Should().HaveCount(3); @@ -352,9 +353,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); responseDocument.Included.Should().HaveCount(2); @@ -364,7 +365,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[1].Type.Should().Be("comments"); responseDocument.Included[1].Id.Should().Be(blog.Posts[0].Comments.Single().StringId); - responseDocument.Included[1].Attributes["createdAt"].Should().BeCloseTo(blog.Posts[0].Comments.Single().CreatedAt); + responseDocument.Included[1].Attributes["createdAt"].As().Should().BeCloseTo(blog.Posts[0].Comments.Single().CreatedAt); } [Fact] @@ -391,9 +392,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(comment.StringId); - responseDocument.SingleData.Attributes["text"].Should().Be(comment.Text); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(comment.StringId); + responseDocument.Data.SingleValue.Attributes["text"].Should().Be(comment.Text); responseDocument.Included.Should().HaveCount(5); @@ -444,9 +445,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); responseDocument.Included.Should().HaveCount(7); @@ -503,9 +504,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -538,7 +539,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("webAccounts"); @@ -550,20 +551,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_include_unknown_relationship() { // Arrange - const string route = "/webAccounts?include=doesNotExist"; + string route = $"/webAccounts?include={Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified include is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); error.Source.Parameter.Should().Be("include"); } @@ -571,20 +572,20 @@ public async Task Cannot_include_unknown_relationship() public async Task Cannot_include_unknown_nested_relationship() { // Arrange - const string route = "/blogs?include=posts.doesNotExist"; + string route = $"/blogs?include=posts.{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified include is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); error.Source.Parameter.Should().Be("include"); } @@ -595,14 +596,14 @@ public async Task Cannot_include_relationship_with_blocked_capability() const string route = "/blogPosts?include=parent"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Including the requested relationship is not allowed."); error.Detail.Should().Be("Including the relationship 'parent' on 'blogPosts' is not allowed."); @@ -631,16 +632,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject[] postWithReviewer = responseDocument.ManyData - .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.SingleData != null).ToArray(); + ResourceObject[] postWithReviewer = responseDocument.Data.ManyValue + .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.Data.SingleValue != null).ToArray(); postWithReviewer.Should().HaveCount(1); postWithReviewer[0].Attributes["caption"].Should().Be(posts[0].Caption); - ResourceObject[] postWithoutReviewer = responseDocument.ManyData - .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.SingleData == null).ToArray(); + ResourceObject[] postWithoutReviewer = responseDocument.Data.ManyValue + .Where(resource => resource.Relationships.First(pair => pair.Key == "reviewer").Value.Data.SingleValue == null).ToArray(); postWithoutReviewer.Should().HaveCount(1); postWithoutReviewer[0].Attributes["caption"].Should().Be(posts[1].Caption); @@ -685,14 +686,14 @@ public async Task Cannot_exceed_configured_maximum_inclusion_depth() const string route = "/blogs/123/owner?include=posts.comments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified include is invalid."); error.Detail.Should().Be("Including 'posts.comments' exceeds the maximum inclusion depth of 1."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs index 344faf6a0c..5c888227e7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/LabelColor.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings { [UsedImplicitly(ImplicitUseTargetFlags.Members)] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum LabelColor { Red, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs index 9d015e7fb6..8d87adfc7d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithTotalCountTests.cs @@ -60,12 +60,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?page[size]=1"); responseDocument.Links.Last.Should().Be(responseDocument.Links.Self); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -86,14 +86,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}?page[number]=2"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -121,15 +121,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/posts?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); - responseDocument.Links.Next.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[number]=3&page[size]=1"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/posts?page[number]=3&page[size]=1"); } [Fact] @@ -147,14 +147,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}/author?page[size]=5"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -184,16 +184,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blogs[0].Posts[1].StringId); responseDocument.Included[1].Id.Should().Be(blogs[1].Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogs?include=posts&page[size]=2,posts:1"); - responseDocument.Links.Last.Should().Be(HostPrefix + "/blogs?include=posts&page[number]=2&page[size]=2,posts:1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogs?include=posts&page[size]=2,posts:1"); + responseDocument.Links.Last.Should().Be($"{HostPrefix}/blogs?include=posts&page[number]=2&page[size]=2,posts:1"); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().Be(responseDocument.Links.Last); } @@ -220,12 +220,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -253,12 +253,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/relationships/posts?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/relationships/posts?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -292,15 +292,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(posts[0].Labels.ElementAt(1).StringId); responseDocument.Included[1].Id.Should().Be(posts[1].Labels.ElementAt(1).StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?include=labels&page[size]=labels:1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?include=labels&page[size]=labels:1"); responseDocument.Links.Last.Should().Be(responseDocument.Links.First); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -328,12 +328,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.Labels.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.Labels.ElementAt(1).StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/blogPosts/{post.StringId}/relationships/labels?page[size]=1"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts/{post.StringId}/relationships/labels?page[size]=1"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); @@ -364,20 +364,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); responseDocument.Included[1].Id.Should().Be(blogs[1].Owner.Posts[1].StringId); responseDocument.Included[2].Id.Should().Be(blogs[1].Owner.Posts[1].Comments.ElementAt(1).StringId); - const string linkPrefix = HostPrefix + "/blogs?include=owner.posts.comments"; + string linkPrefix = $"{HostPrefix}/blogs?include=owner.posts.comments"; responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(linkPrefix + "&page[size]=1,owner.posts:1,owner.posts.comments:1"); - responseDocument.Links.Last.Should().Be(linkPrefix + "&page[size]=1,owner.posts:1,owner.posts.comments:1&page[number]=2"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{linkPrefix}&page[size]=1,owner.posts:1,owner.posts.comments:1"); + responseDocument.Links.Last.Should().Be($"{linkPrefix}&page[size]=1,owner.posts:1,owner.posts.comments:1&page[number]=2"); responseDocument.Links.Prev.Should().Be(responseDocument.Links.First); responseDocument.Links.Next.Should().BeNull(); } @@ -386,20 +386,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_paginate_in_unknown_scope() { // Arrange - const string route = "/webAccounts?page[number]=doesNotExist:1"; + string route = $"/webAccounts?page[number]={Unknown.Relationship}:1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); error.Source.Parameter.Should().Be("page[number]"); } @@ -407,20 +407,20 @@ public async Task Cannot_paginate_in_unknown_scope() public async Task Cannot_paginate_in_unknown_nested_scope() { // Arrange - const string route = "/webAccounts?page[size]=posts.doesNotExist:1"; + string route = $"/webAccounts?page[size]=posts.{Unknown.Relationship}:1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); error.Source.Parameter.Should().Be("page[size]"); } @@ -448,16 +448,16 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blog.Posts[1].StringId); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().Be(responseDocument.Links.Self); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); - responseDocument.Links.Next.Should().Be(HostPrefix + $"/blogs/{blog.StringId}/posts?page[number]=2"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/blogs/{blog.StringId}/posts?page[number]=2"); } [Fact] @@ -484,10 +484,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(25); + responseDocument.Data.ManyValue.Should().HaveCount(25); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -503,7 +503,7 @@ public async Task Renders_correct_top_level_links_for_page_number(int pageNumber { // Arrange WebAccount account = _fakers.WebAccount.Generate(); - account.UserName = "&" + account.UserName; + account.UserName = $"&{account.UserName}"; const int totalCount = 3 * DefaultPageSize + 3; List posts = _fakers.BlogPost.Generate(totalCount); @@ -520,10 +520,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string routePrefix = "/blogPosts?filter=equals(author.userName,'" + WebUtility.UrlEncode(account.UserName) + "')" + + string routePrefix = $"/blogPosts?filter=equals(author.userName,'{WebUtility.UrlEncode(account.UserName)}')" + "&fields[webAccounts]=userName&include=author&sort=id&foo=bar,baz"; - string route = routePrefix + $"&page[number]={pageNumber}"; + string route = $"{routePrefix}&page[number]={pageNumber}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -531,11 +531,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); if (firstLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, firstLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, firstLink.Value)}"; responseDocument.Links.First.Should().Be(expected); } else @@ -545,7 +545,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (prevLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, prevLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, prevLink.Value)}"; responseDocument.Links.Prev.Should().Be(expected); } else @@ -555,7 +555,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (nextLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, nextLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, nextLink.Value)}"; responseDocument.Links.Next.Should().Be(expected); } else @@ -565,7 +565,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => if (lastLink != null) { - string expected = HostPrefix + SetPageNumberInUrl(routePrefix, lastLink.Value); + string expected = $"{HostPrefix}{SetPageNumberInUrl(routePrefix, lastLink.Value)}"; responseDocument.Links.Last.Should().Be(expected); } else @@ -575,7 +575,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => static string SetPageNumberInUrl(string url, int pageNumber) { - return pageNumber != 1 ? url + "&page[number]=" + pageNumber : url; + return pageNumber != 1 ? $"{url}&page[number]={pageNumber}" : url; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs index 9ff4accd59..89ebae123e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/PaginationWithoutTotalCountTests.cs @@ -48,7 +48,7 @@ public async Task Hides_pagination_links_when_unconstrained_page_size() httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.First.Should().BeNull(); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); @@ -77,8 +77,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + route); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}{route}"); responseDocument.Links.Last.Should().BeNull(); responseDocument.Links.Prev.Should().BeNull(); responseDocument.Links.Next.Should().BeNull(); @@ -103,10 +103,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Next.Should().BeNull(); } @@ -131,13 +131,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Count.Should().BeLessThan(DefaultPageSize); + responseDocument.Data.ManyValue.Count.Should().BeLessThan(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?foo=bar&page[number]=2"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/blogPosts?foo=bar&page[number]=2"); responseDocument.Links.Next.Should().BeNull(); } @@ -162,14 +162,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(DefaultPageSize); + responseDocument.Data.ManyValue.Should().HaveCount(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + "/blogPosts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/blogPosts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + "/blogPosts?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be(HostPrefix + "/blogPosts?page[number]=4&foo=bar"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/blogPosts?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/blogPosts?page[number]=4&foo=bar"); } [Fact] @@ -193,14 +193,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(DefaultPageSize); + responseDocument.Data.ManyValue.Should().HaveCount(DefaultPageSize); responseDocument.Links.Should().NotBeNull(); - responseDocument.Links.Self.Should().Be(HostPrefix + route); - responseDocument.Links.First.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?foo=bar"); + responseDocument.Links.Self.Should().Be($"{HostPrefix}{route}"); + responseDocument.Links.First.Should().Be($"{HostPrefix}/webAccounts/{account.StringId}/posts?foo=bar"); responseDocument.Links.Last.Should().BeNull(); - responseDocument.Links.Prev.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?page[number]=2&foo=bar"); - responseDocument.Links.Next.Should().Be(HostPrefix + $"/webAccounts/{account.StringId}/posts?page[number]=4&foo=bar"); + responseDocument.Links.Prev.Should().Be($"{HostPrefix}/webAccounts/{account.StringId}/posts?page[number]=2&foo=bar"); + responseDocument.Links.Next.Should().Be($"{HostPrefix}/webAccounts/{account.StringId}/posts?page[number]=4&foo=bar"); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs index c6f576c94d..6186df580e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationTests.cs @@ -36,14 +36,14 @@ public async Task Cannot_use_negative_page_number() const string route = "/blogs?page[number]=-1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page number cannot be negative or zero."); @@ -57,14 +57,14 @@ public async Task Cannot_use_zero_page_number() const string route = "/blogs?page[number]=0"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page number cannot be negative or zero."); @@ -105,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] @@ -115,14 +115,14 @@ public async Task Cannot_use_negative_page_size() const string route = "/blogs?page[size]=-1"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page size cannot be negative."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs index 583ae0d712..6d10840d7f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Pagination/RangeValidationWithMaximumTests.cs @@ -33,7 +33,7 @@ public async Task Can_use_page_number_below_maximum() { // Arrange const int pageNumber = MaximumPageNumber - 1; - string route = "/blogs?page[number]=" + pageNumber; + string route = $"/blogs?page[number]={pageNumber}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -47,7 +47,7 @@ public async Task Can_use_page_number_equal_to_maximum() { // Arrange const int pageNumber = MaximumPageNumber; - string route = "/blogs?page[number]=" + pageNumber; + string route = $"/blogs?page[number]={pageNumber}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); @@ -61,17 +61,17 @@ public async Task Cannot_use_page_number_over_maximum() { // Arrange const int pageNumber = MaximumPageNumber + 1; - string route = "/blogs?page[number]=" + pageNumber; + string route = $"/blogs?page[number]={pageNumber}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be($"Page number cannot be higher than {MaximumPageNumber}."); @@ -85,14 +85,14 @@ public async Task Cannot_use_zero_page_size() const string route = "/blogs?page[size]=0"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be("Page size cannot be unconstrained."); @@ -104,10 +104,10 @@ public async Task Can_use_page_size_below_maximum() { // Arrange const int pageSize = MaximumPageSize - 1; - string route = "/blogs?page[size]=" + pageSize; + string route = $"/blogs?page[size]={pageSize}"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -118,10 +118,10 @@ public async Task Can_use_page_size_equal_to_maximum() { // Arrange const int pageSize = MaximumPageSize; - string route = "/blogs?page[size]=" + pageSize; + string route = $"/blogs?page[size]={pageSize}"; // Act - (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); @@ -132,17 +132,17 @@ public async Task Cannot_use_page_size_over_maximum() { // Arrange const int pageSize = MaximumPageSize + 1; - string route = "/blogs?page[size]=" + pageSize; + string route = $"/blogs?page[size]={pageSize}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified paging is invalid."); error.Detail.Should().Be($"Page size cannot be higher than {MaximumPageSize}."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs index 98c9bd85fa..eae4e173f3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/QueryStringTests.cs @@ -31,14 +31,14 @@ public async Task Cannot_use_unknown_query_string_parameter() const string route = "/calendars?foo=bar"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Unknown query string parameter."); @@ -70,25 +70,23 @@ public async Task Can_use_unknown_query_string_parameter() [InlineData("sort")] [InlineData("page[size]")] [InlineData("page[number]")] - [InlineData("defaults")] - [InlineData("nulls")] public async Task Cannot_use_empty_query_string_parameter_value(string parameterName) { // Arrange var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); options.AllowUnknownQueryStringParameters = false; - string route = "calendars?" + parameterName + "="; + string route = $"calendars?{parameterName}="; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing query string parameter value."); error.Detail.Should().Be($"Missing value for '{parameterName}' query string parameter."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs deleted file mode 100644 index 6ac9eb4ac1..0000000000 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerDefaultValueHandlingTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using TestBuildingBlocks; -using Xunit; - -namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings -{ - public sealed class SerializerDefaultValueHandlingTests : IClassFixture, QueryStringDbContext>> - { - private readonly IntegrationTestContext, QueryStringDbContext> _testContext; - private readonly QueryStringFakers _fakers = new(); - - public SerializerDefaultValueHandlingTests(IntegrationTestContext, QueryStringDbContext> testContext) - { - _testContext = testContext; - - testContext.UseController(); - } - - [Fact] - public async Task Cannot_override_from_query_string() - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerDefaultValueHandling = false; - - const string route = "/calendars?defaults=true"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - error.Detail.Should().Be("The parameter 'defaults' cannot be used at this endpoint."); - error.Source.Parameter.Should().Be("defaults"); - } - - [Theory] - [InlineData(null, null, true)] - [InlineData(null, "false", false)] - [InlineData(null, "true", true)] - [InlineData(DefaultValueHandling.Ignore, null, false)] - [InlineData(DefaultValueHandling.Ignore, "false", false)] - [InlineData(DefaultValueHandling.Ignore, "true", true)] - [InlineData(DefaultValueHandling.Include, null, true)] - [InlineData(DefaultValueHandling.Include, "false", false)] - [InlineData(DefaultValueHandling.Include, "true", true)] - public async Task Can_override_from_query_string(DefaultValueHandling? configurationValue, string queryStringValue, bool expectInDocument) - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerDefaultValueHandling = true; - options.SerializerSettings.DefaultValueHandling = configurationValue ?? DefaultValueHandling.Include; - - Calendar calendar = _fakers.Calendar.Generate(); - calendar.DefaultAppointmentDurationInMinutes = default; - calendar.Appointments = _fakers.Appointment.Generate(1).ToHashSet(); - calendar.Appointments.Single().EndTime = default; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Calendars.Add(calendar); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/calendars/{calendar.StringId}?include=appointments" + (queryStringValue != null ? "&defaults=" + queryStringValue : ""); - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.Included.Should().HaveCount(1); - - if (expectInDocument) - { - responseDocument.SingleData.Attributes.Should().ContainKey("defaultAppointmentDurationInMinutes"); - responseDocument.Included[0].Attributes.Should().ContainKey("endTime"); - } - else - { - responseDocument.SingleData.Attributes.Should().NotContainKey("defaultAppointmentDurationInMinutes"); - responseDocument.Included[0].Attributes.Should().NotContainKey("endTime"); - } - } - } -} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs new file mode 100644 index 0000000000..e8d790446c --- /dev/null +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerIgnoreConditionTests.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Serialization.Objects; +using Microsoft.Extensions.DependencyInjection; +using TestBuildingBlocks; +using Xunit; + +namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings +{ + public sealed class SerializerIgnoreConditionTests : IntegrationTestContext, QueryStringDbContext> + { + private readonly QueryStringFakers _fakers = new(); + + public SerializerIgnoreConditionTests() + { + UseController(); + } + + [Theory] + [InlineData(JsonIgnoreCondition.Never, true, true)] + [InlineData(JsonIgnoreCondition.WhenWritingDefault, false, false)] + [InlineData(JsonIgnoreCondition.WhenWritingNull, false, true)] + public async Task Applies_configuration_for_ignore_condition(JsonIgnoreCondition configurationValue, bool expectNullValueInDocument, + bool expectDefaultValueInDocument) + { + // Arrange + var options = (JsonApiOptions)Factory.Services.GetRequiredService(); + options.SerializerOptions.DefaultIgnoreCondition = configurationValue; + + Calendar calendar = _fakers.Calendar.Generate(); + calendar.TimeZone = null; + calendar.DefaultAppointmentDurationInMinutes = default; + calendar.ShowWeekNumbers = true; + calendar.Appointments = _fakers.Appointment.Generate(1).ToHashSet(); + calendar.Appointments.Single().Title = null; + calendar.Appointments.Single().StartTime = default; + calendar.Appointments.Single().EndTime = 1.January(2001); + + await RunOnDatabaseAsync(async dbContext => + { + dbContext.Calendars.Add(calendar); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/calendars/{calendar.StringId}?include=appointments"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Included.Should().HaveCount(1); + + if (expectNullValueInDocument) + { + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("timeZone"); + responseDocument.Included[0].Attributes.Should().ContainKey("title"); + } + else + { + responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("timeZone"); + responseDocument.Included[0].Attributes.Should().NotContainKey("title"); + } + + if (expectDefaultValueInDocument) + { + responseDocument.Data.SingleValue.Attributes.Should().ContainKey("defaultAppointmentDurationInMinutes"); + responseDocument.Included[0].Attributes.Should().ContainKey("startTime"); + } + else + { + responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("defaultAppointmentDurationInMinutes"); + responseDocument.Included[0].Attributes.Should().NotContainKey("startTime"); + } + } + } +} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs deleted file mode 100644 index 3217e06c2a..0000000000 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SerializerNullValueHandlingTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Serialization.Objects; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using TestBuildingBlocks; -using Xunit; - -namespace JsonApiDotNetCoreTests.IntegrationTests.QueryStrings -{ - public sealed class SerializerNullValueHandlingTests : IClassFixture, QueryStringDbContext>> - { - private readonly IntegrationTestContext, QueryStringDbContext> _testContext; - private readonly QueryStringFakers _fakers = new(); - - public SerializerNullValueHandlingTests(IntegrationTestContext, QueryStringDbContext> testContext) - { - _testContext = testContext; - - testContext.UseController(); - } - - [Fact] - public async Task Cannot_override_from_query_string() - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerNullValueHandling = false; - - const string route = "/calendars?nulls=true"; - - // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); - - responseDocument.Errors.Should().HaveCount(1); - - Error error = responseDocument.Errors[0]; - error.StatusCode.Should().Be(HttpStatusCode.BadRequest); - error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); - error.Detail.Should().Be("The parameter 'nulls' cannot be used at this endpoint."); - error.Source.Parameter.Should().Be("nulls"); - } - - [Theory] - [InlineData(null, null, true)] - [InlineData(null, "false", false)] - [InlineData(null, "true", true)] - [InlineData(NullValueHandling.Ignore, null, false)] - [InlineData(NullValueHandling.Ignore, "false", false)] - [InlineData(NullValueHandling.Ignore, "true", true)] - [InlineData(NullValueHandling.Include, null, true)] - [InlineData(NullValueHandling.Include, "false", false)] - [InlineData(NullValueHandling.Include, "true", true)] - public async Task Can_override_from_query_string(NullValueHandling? configurationValue, string queryStringValue, bool expectInDocument) - { - // Arrange - var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); - options.AllowQueryStringOverrideForSerializerNullValueHandling = true; - options.SerializerSettings.NullValueHandling = configurationValue ?? NullValueHandling.Include; - - Calendar calendar = _fakers.Calendar.Generate(); - calendar.TimeZone = null; - calendar.Appointments = _fakers.Appointment.Generate(1).ToHashSet(); - calendar.Appointments.Single().Title = null; - - await _testContext.RunOnDatabaseAsync(async dbContext => - { - dbContext.Calendars.Add(calendar); - await dbContext.SaveChangesAsync(); - }); - - string route = $"/calendars/{calendar.StringId}?include=appointments" + (queryStringValue != null ? "&nulls=" + queryStringValue : ""); - - // Act - (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); - - // Assert - httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.Included.Should().HaveCount(1); - - if (expectInDocument) - { - responseDocument.SingleData.Attributes.Should().ContainKey("timeZone"); - responseDocument.Included[0].Attributes.Should().ContainKey("title"); - } - else - { - responseDocument.SingleData.Attributes.Should().NotContainKey("timeZone"); - responseDocument.Included[0].Attributes.Should().NotContainKey("title"); - } - } - } -} diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs index c70b793b59..43ceb3fdf9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/Sorting/SortTests.cs @@ -49,10 +49,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(posts[2].StringId); } [Fact] @@ -70,14 +70,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}?sort=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -108,10 +108,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(blog.Posts[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(blog.Posts[2].StringId); } [Fact] @@ -129,14 +129,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/blogPosts/{post.StringId}/author?sort=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); error.Detail.Should().Be("This query string parameter can only be used on a collection of resources (not on a single resource)."); @@ -166,9 +166,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blogs[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); } [Fact] @@ -194,9 +194,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); } [Fact] @@ -223,8 +223,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(account.Posts[1].StringId); @@ -257,8 +257,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.Owner.StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(blog.Owner.Posts[1].StringId); @@ -290,8 +290,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included[0].Id.Should().Be(post.Labels.ElementAt(1).StringId); @@ -337,9 +337,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blogs[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); responseDocument.Included.Should().HaveCount(7); @@ -391,9 +391,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(posts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(posts[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(posts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(posts[0].StringId); } [Fact] @@ -428,9 +428,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(blogs[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(blogs[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(blogs[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(blogs[0].StringId); responseDocument.Included.Should().HaveCount(5); responseDocument.Included[0].Id.Should().Be(blogs[1].Owner.StringId); @@ -444,42 +444,42 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_sort_in_unknown_scope() { // Arrange - const string route = "/webAccounts?sort[doesNotExist]=id"; + string route = $"/webAccounts?sort[{Unknown.Relationship}]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' does not exist on resource 'webAccounts'."); - error.Source.Parameter.Should().Be("sort[doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' does not exist on resource 'webAccounts'."); + error.Source.Parameter.Should().Be($"sort[{Unknown.Relationship}]"); } [Fact] public async Task Cannot_sort_in_unknown_nested_scope() { // Arrange - const string route = "/webAccounts?sort[posts.doesNotExist]=id"; + string route = $"/webAccounts?sort[posts.{Unknown.Relationship}]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified sort is invalid."); - error.Detail.Should().Be("Relationship 'doesNotExist' in 'posts.doesNotExist' does not exist on resource 'blogPosts'."); - error.Source.Parameter.Should().Be("sort[posts.doesNotExist]"); + error.Detail.Should().Be($"Relationship '{Unknown.Relationship}' in 'posts.{Unknown.Relationship}' does not exist on resource 'blogPosts'."); + error.Source.Parameter.Should().Be($"sort[posts.{Unknown.Relationship}]"); } [Fact] @@ -489,14 +489,14 @@ public async Task Cannot_sort_on_attribute_with_blocked_capability() const string route = "/webAccounts?sort=dateOfBirth"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Sorting on the requested attribute is not allowed."); error.Detail.Should().Be("Sorting on attribute 'dateOfBirth' is not allowed."); @@ -531,10 +531,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(accounts[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(accounts[2].StringId); - responseDocument.ManyData[2].Id.Should().Be(accounts[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(accounts[2].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(accounts[0].StringId); } [Fact] @@ -562,11 +562,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(4); - responseDocument.ManyData[0].Id.Should().Be(accounts[2].StringId); - responseDocument.ManyData[1].Id.Should().Be(accounts[1].StringId); - responseDocument.ManyData[2].Id.Should().Be(accounts[0].StringId); - responseDocument.ManyData[3].Id.Should().Be(accounts[3].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(4); + responseDocument.Data.ManyValue[0].Id.Should().Be(accounts[2].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(accounts[1].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(accounts[0].StringId); + responseDocument.Data.ManyValue[3].Id.Should().Be(accounts[3].StringId); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs index 14ac363c02..a8681a1403 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/SparseFieldSets/SparseFieldSetTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Net; using System.Net.Http; @@ -58,14 +59,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["author"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().Be(post.Caption); @@ -96,11 +97,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().Be(post.Caption); @@ -131,13 +132,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["author"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.ManyValue[0].Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Caption.Should().BeNull(); @@ -168,14 +169,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(blog.Posts[0].StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); - responseDocument.ManyData[0].Relationships.Should().HaveCount(1); - responseDocument.ManyData[0].Relationships["labels"].Data.Should().BeNull(); - responseDocument.ManyData[0].Relationships["labels"].Links.Self.Should().NotBeNull(); - responseDocument.ManyData[0].Relationships["labels"].Links.Related.Should().NotBeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(blog.Posts[0].StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(blog.Posts[0].Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Relationships["labels"].Data.Value.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships["labels"].Links.Self.Should().NotBeNull(); + responseDocument.Data.ManyValue[0].Relationships["labels"].Links.Related.Should().NotBeNull(); var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.Id.Should().Be(blog.Id); @@ -209,14 +210,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["url"].Should().Be(post.Url); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["author"].Data.Should().BeNull(); - responseDocument.SingleData.Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["url"].Should().Be(post.Url); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Url.Should().Be(post.Url); @@ -247,19 +248,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); - responseDocument.SingleData.Relationships["author"].SingleData.Id.Should().Be(post.Author.StringId); - responseDocument.SingleData.Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Relationships["author"].Data.SingleValue.Id.Should().Be(post.Author.StringId); + responseDocument.Data.SingleValue.Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(2); responseDocument.Included[0].Attributes["displayName"].Should().Be(post.Author.DisplayName); responseDocument.Included[0].Attributes["emailAddress"].Should().Be(post.Author.EmailAddress); responseDocument.Included[0].Relationships.Should().HaveCount(1); - responseDocument.Included[0].Relationships["preferences"].Data.Should().BeNull(); + responseDocument.Included[0].Relationships["preferences"].Data.Value.Should().BeNull(); responseDocument.Included[0].Relationships["preferences"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["preferences"].Links.Related.Should().NotBeNull(); @@ -296,19 +297,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(account.StringId); - responseDocument.SingleData.Attributes["displayName"].Should().Be(account.DisplayName); - responseDocument.SingleData.Relationships["posts"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["posts"].ManyData[0].Id.Should().Be(account.Posts[0].StringId); - responseDocument.SingleData.Relationships["posts"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["posts"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(account.StringId); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(account.DisplayName); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Id.Should().Be(account.Posts[0].StringId); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(1); responseDocument.Included[0].Attributes["caption"].Should().Be(account.Posts[0].Caption); responseDocument.Included[0].Relationships.Should().HaveCount(1); - responseDocument.Included[0].Relationships["labels"].Data.Should().BeNull(); + responseDocument.Included[0].Relationships["labels"].Data.Value.Should().BeNull(); responseDocument.Included[0].Relationships["labels"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["labels"].Links.Related.Should().NotBeNull(); @@ -346,19 +347,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.Owner.StringId); - responseDocument.SingleData.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); - responseDocument.SingleData.Relationships["posts"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["posts"].ManyData[0].Id.Should().Be(blog.Owner.Posts[0].StringId); - responseDocument.SingleData.Relationships["posts"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["posts"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Attributes["displayName"].Should().Be(blog.Owner.DisplayName); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Owner.Posts[0].StringId); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["posts"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(1); responseDocument.Included[0].Attributes["caption"].Should().Be(blog.Owner.Posts[0].Caption); responseDocument.Included[0].Relationships.Should().HaveCount(1); - responseDocument.Included[0].Relationships["comments"].Data.Should().BeNull(); + responseDocument.Included[0].Relationships["comments"].Data.Value.Should().BeNull(); responseDocument.Included[0].Relationships["comments"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["comments"].Links.Related.Should().NotBeNull(); @@ -396,17 +397,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); - responseDocument.SingleData.Relationships["labels"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["labels"].ManyData[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); - responseDocument.SingleData.Relationships["labels"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["labels"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Relationships["labels"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["labels"].Data.ManyValue[0].Id.Should().Be(post.Labels.ElementAt(0).StringId); + responseDocument.Data.SingleValue.Relationships["labels"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["labels"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Attributes.Should().HaveCount(1); - responseDocument.Included[0].Attributes["color"].Should().Be(post.Labels.Single().Color.ToString("G")); + responseDocument.Included[0].Attributes["color"].Should().Be(post.Labels.Single().Color); responseDocument.Included[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); @@ -443,11 +444,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); responseDocument.Included.Should().HaveCount(2); @@ -501,30 +502,30 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["title"].Should().Be(blog.Title); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["owner"].SingleData.Id.Should().Be(blog.Owner.StringId); - responseDocument.SingleData.Relationships["owner"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["owner"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(blog.Title); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["owner"].Data.SingleValue.Id.Should().Be(blog.Owner.StringId); + responseDocument.Data.SingleValue.Relationships["owner"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["owner"].Links.Related.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(2); responseDocument.Included[0].Id.Should().Be(blog.Owner.StringId); responseDocument.Included[0].Attributes["userName"].Should().Be(blog.Owner.UserName); responseDocument.Included[0].Attributes["displayName"].Should().Be(blog.Owner.DisplayName); - responseDocument.Included[0].Attributes["dateOfBirth"].Should().BeCloseTo(blog.Owner.DateOfBirth); - responseDocument.Included[0].Relationships["posts"].ManyData.Should().HaveCount(1); - responseDocument.Included[0].Relationships["posts"].ManyData[0].Id.Should().Be(blog.Owner.Posts[0].StringId); + responseDocument.Included[0].Attributes["dateOfBirth"].As().Should().BeCloseTo(blog.Owner.DateOfBirth.GetValueOrDefault()); + responseDocument.Included[0].Relationships["posts"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Included[0].Relationships["posts"].Data.ManyValue[0].Id.Should().Be(blog.Owner.Posts[0].StringId); responseDocument.Included[0].Relationships["posts"].Links.Self.Should().NotBeNull(); responseDocument.Included[0].Relationships["posts"].Links.Related.Should().NotBeNull(); responseDocument.Included[1].Id.Should().Be(blog.Owner.Posts[0].StringId); responseDocument.Included[1].Attributes["caption"].Should().Be(blog.Owner.Posts[0].Caption); responseDocument.Included[1].Attributes["url"].Should().Be(blog.Owner.Posts[0].Url); - responseDocument.Included[1].Relationships["labels"].Data.Should().BeNull(); + responseDocument.Included[1].Relationships["labels"].Data.Value.Should().BeNull(); responseDocument.Included[1].Relationships["labels"].Links.Self.Should().NotBeNull(); responseDocument.Included[1].Relationships["labels"].Links.Related.Should().NotBeNull(); @@ -558,11 +559,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["caption"].Should().Be(post.Caption); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); @@ -594,10 +595,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(post.StringId); - responseDocument.ManyData[0].Attributes.Should().BeNull(); - responseDocument.ManyData[0].Relationships.Should().BeNull(); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(post.StringId); + responseDocument.Data.ManyValue[0].Attributes.Should().BeNull(); + responseDocument.Data.ManyValue[0].Relationships.Should().BeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); @@ -608,21 +609,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_select_on_unknown_resource_type() { // Arrange - const string route = "/webAccounts?fields[doesNotExist]=id"; + string route = $"/webAccounts?fields[{Unknown.ResourceType}]=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("The specified fieldset is invalid."); - error.Detail.Should().Be("Resource type 'doesNotExist' does not exist."); - error.Source.Parameter.Should().Be("fields[doesNotExist]"); + error.Detail.Should().Be($"Resource type '{Unknown.ResourceType}' does not exist."); + error.Source.Parameter.Should().Be($"fields[{Unknown.ResourceType}]"); } [Fact] @@ -634,14 +635,14 @@ public async Task Cannot_select_attribute_with_blocked_capability() string route = $"/webAccounts/{account.Id}?fields[webAccounts]=password"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Retrieving the requested attribute is not allowed."); error.Detail.Should().Be("Retrieving the attribute 'password' is not allowed."); @@ -671,11 +672,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(blog.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["showAdvertisements"].Should().Be(blog.ShowAdvertisements); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(blog.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["showAdvertisements"].Should().Be(blog.ShowAdvertisements); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); var blogCaptured = (Blog)store.Resources.Should().ContainSingle(resource => resource is Blog).And.Subject.Single(); blogCaptured.ShowAdvertisements.Should().Be(blogCaptured.ShowAdvertisements); @@ -705,15 +706,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(post.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["caption"].Should().Be(post.Caption); - responseDocument.SingleData.Attributes["url"].Should().Be(post.Url); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["author"].Data.Should().BeNull(); - responseDocument.SingleData.Relationships["author"].Links.Self.Should().NotBeNull(); - responseDocument.SingleData.Relationships["author"].Links.Related.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(post.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["caption"].Should().Be(post.Caption); + responseDocument.Data.SingleValue.Attributes["url"].Should().Be(post.Url); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["author"].Data.Value.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Self.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["author"].Links.Related.Should().NotBeNull(); var postCaptured = (BlogPost)store.Resources.Should().ContainSingle(resource => resource is BlogPost).And.Subject.Single(); postCaptured.Id.Should().Be(post.Id); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs index 6bbb3ce71c..9a148f48b1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceTests.cs @@ -61,14 +61,14 @@ public async Task Sets_location_header_for_created_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - string newWorkItemId = responseDocument.SingleData.Id; - httpResponse.Headers.Location.Should().Be("/workItems/" + newWorkItemId); + string newWorkItemId = responseDocument.Data.SingleValue.Id; + httpResponse.Headers.Location.Should().Be($"/workItems/{newWorkItemId}"); responseDocument.Links.Self.Should().Be("http://localhost/workItems"); responseDocument.Links.First.Should().BeNull(); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Links.Self.Should().Be("http://localhost" + httpResponse.Headers.Location); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Links.Self.Should().Be($"http://localhost{httpResponse.Headers.Location}"); } [Fact] @@ -98,13 +98,13 @@ public async Task Can_create_resource_with_int_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Attributes["dueAt"].Should().Be(newWorkItem.DueAt); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -145,13 +145,13 @@ public async Task Can_create_resource_with_long_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("userAccounts"); - responseDocument.SingleData.Attributes["firstName"].Should().Be(newUserAccount.FirstName); - responseDocument.SingleData.Attributes["lastName"].Should().Be(newUserAccount.LastName); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("userAccounts"); + responseDocument.Data.SingleValue.Attributes["firstName"].Should().Be(newUserAccount.FirstName); + responseDocument.Data.SingleValue.Attributes["lastName"].Should().Be(newUserAccount.LastName); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - long newUserAccountId = long.Parse(responseDocument.SingleData.Id); + long newUserAccountId = long.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -191,12 +191,12 @@ public async Task Can_create_resource_with_guid_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(newGroup.Name); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - Guid newGroupId = Guid.Parse(responseDocument.SingleData.Id); + Guid newGroupId = Guid.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -235,13 +235,13 @@ public async Task Can_create_resource_without_attributes_or_relationships() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes["description"].Should().BeNull(); - responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes["description"].Should().BeNull(); + responseDocument.Data.SingleValue.Attributes["dueAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -279,12 +279,12 @@ public async Task Can_create_resource_with_unknown_attribute() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -309,8 +309,8 @@ public async Task Can_create_resource_with_unknown_relationship() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } @@ -325,12 +325,12 @@ public async Task Can_create_resource_with_unknown_relationship() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -360,14 +360,14 @@ public async Task Cannot_create_resource_with_client_generated_ID() const string route = "/rgbColors"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Specifying the resource ID in POST requests is not allowed."); error.Detail.Should().BeNull(); @@ -383,14 +383,14 @@ public async Task Cannot_create_resource_for_missing_request_body() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -413,14 +413,14 @@ public async Task Cannot_create_resource_for_missing_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -434,7 +434,7 @@ public async Task Cannot_create_resource_for_unknown_type() { data = new { - type = "doesNotExist", + type = Unknown.ResourceType, attributes = new { } @@ -444,17 +444,17 @@ public async Task Cannot_create_resource_for_unknown_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -472,7 +472,7 @@ public async Task Cannot_create_resource_on_unknown_resource_type_in_url() } }; - const string route = "/doesNotExist"; + const string route = "/" + Unknown.ResourceType; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -499,14 +499,14 @@ public async Task Cannot_create_on_resource_type_mismatch_between_url_and_body() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); error.Detail.Should().Be("Expected resource of type 'workItems' in POST request body at endpoint '/workItems', instead of 'rgbColors'."); @@ -531,17 +531,17 @@ public async Task Cannot_create_resource_attribute_with_blocked_capability() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Setting the initial value of the requested attribute is not allowed."); - error.Detail.Should().StartWith("Setting the initial value of 'isImportant' is not allowed. - Request body:"); + error.Detail.Should().StartWith("Setting the initial value of 'isImportant' is not allowed. - Request body: <<"); } [Fact] @@ -563,17 +563,17 @@ public async Task Cannot_create_resource_with_readonly_attribute() const string route = "/workItemGroups"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body:"); + error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body: <<"); } [Fact] @@ -585,17 +585,17 @@ public async Task Cannot_create_resource_for_broken_JSON_request_body() const string route = "/workItemGroups"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Invalid character after parsing"); + error.Detail.Should().Match("'{' is invalid after a property name. * - Request body: <<*"); } [Fact] @@ -617,17 +617,19 @@ public async Task Cannot_create_resource_with_incompatible_attribute_value() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + + error.Detail.Should().StartWith("Failed to convert attribute 'dueAt' with value 'not-a-valid-time' " + + "of type 'String' to type 'Nullable'. - Request body: <<"); } [Fact] @@ -699,11 +701,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newDescription); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs index 3c30d86e83..e8dc30f79f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithClientGeneratedIdTests.cs @@ -63,17 +63,17 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Id.Should().Be(newGroup.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Id.Should().Be(newGroup.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItemGroup groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroup.Id); - groupInDatabase.Name.Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); + groupInDatabase.Name.Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); }); PropertyInfo property = typeof(WorkItemGroup).GetProperty(nameof(Identifiable.Id)); @@ -108,18 +108,18 @@ public async Task Can_create_resource_with_client_generated_guid_ID_having_side_ // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Id.Should().Be(newGroup.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["name"].Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Id.Should().Be(newGroup.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItemGroup groupInDatabase = await dbContext.Groups.FirstWithIdAsync(newGroup.Id); - groupInDatabase.Name.Should().Be(newGroup.Name + ImplicitlyChangingWorkItemGroupDefinition.Suffix); + groupInDatabase.Name.Should().Be($"{newGroup.Name}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); }); PropertyInfo property = typeof(WorkItemGroup).GetProperty(nameof(Identifiable.Id)); @@ -237,14 +237,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/rgbColors"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Another resource with the specified ID already exists."); error.Detail.Should().Be($"Another resource of type 'rgbColors' with ID '{existingColor.StringId}' already exists."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs index f9d1caa667..e9eae47336 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToManyRelationshipTests.cs @@ -72,12 +72,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().BeNull(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -136,9 +136,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(2); responseDocument.Included.Should().OnlyContain(resource => resource.Type == "userAccounts"); @@ -148,7 +148,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().OnlyContain(resource => resource.Attributes["lastName"] != null); responseDocument.Included.Should().OnlyContain(resource => resource.Relationships.Count > 0); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -207,9 +207,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(2); responseDocument.Included.Should().OnlyContain(resource => resource.Type == "userAccounts"); @@ -219,7 +219,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().OnlyContain(resource => resource.Attributes["firstName"] != null); responseDocument.Included.Should().OnlyContain(resource => resource.Relationships == null); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -289,14 +289,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["priority"].Should().Be(workItemToCreate.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData.Should().HaveCount(3); - responseDocument.SingleData.Relationships["tags"].ManyData[0].Id.Should().Be(existingTags[0].StringId); - responseDocument.SingleData.Relationships["tags"].ManyData[1].Id.Should().Be(existingTags[1].StringId); - responseDocument.SingleData.Relationships["tags"].ManyData[2].Id.Should().Be(existingTags[2].StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(workItemToCreate.Priority); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[0].Id.Should().Be(existingTags[0].StringId); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[1].Id.Should().Be(existingTags[1].StringId); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[2].Id.Should().Be(existingTags[2].StringId); responseDocument.Included.Should().HaveCount(3); responseDocument.Included.Should().OnlyContain(resource => resource.Type == "workTags"); @@ -307,7 +307,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included.Should().OnlyContain(resource => resource.Attributes["text"] != null); responseDocument.Included.Should().OnlyContain(resource => resource.Relationships == null); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -337,7 +337,7 @@ public async Task Cannot_create_for_missing_relationship_type() { new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -348,14 +348,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); @@ -378,8 +378,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -390,17 +390,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -431,14 +431,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); @@ -448,6 +448,9 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_for_unknown_relationship_IDs() { // Arrange + string workItemId1 = Unknown.StringId.For(); + string workItemId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new @@ -462,12 +465,12 @@ public async Task Cannot_create_for_unknown_relationship_IDs() new { type = "workItems", - id = 12345678 + id = workItemId1 }, new { type = "workItems", - id = 87654321 + id = workItemId2 } } } @@ -478,22 +481,22 @@ public async Task Cannot_create_for_unknown_relationship_IDs() const string route = "/userAccounts"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workItems' with ID '12345678' in relationship 'assignedItems' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workItems' with ID '{workItemId1}' in relationship 'assignedItems' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workItems' with ID '87654321' in relationship 'assignedItems' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workItems' with ID '{workItemId2}' in relationship 'assignedItems' does not exist."); } [Fact] @@ -525,14 +528,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -585,15 +588,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); responseDocument.Included[0].Id.Should().Be(existingUserAccount.StringId); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -626,14 +629,14 @@ public async Task Cannot_create_with_null_data_in_OneToMany_relationship() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -661,14 +664,14 @@ public async Task Cannot_create_with_null_data_in_ManyToMany_relationship() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); @@ -706,16 +709,16 @@ public async Task Cannot_create_resource_with_local_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); + error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs index 761dfa50df..acd6f6136e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Creating/CreateResourceWithToOneRelationshipTests.cs @@ -2,13 +2,13 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; using TestBuildingBlocks; using Xunit; @@ -72,11 +72,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); - string newGroupId = responseDocument.SingleData.Id; + string newGroupId = responseDocument.Data.SingleValue.Id; newGroupId.Should().NotBeNullOrEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -190,9 +190,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -201,7 +201,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["lastName"].Should().Be(existingUserAccount.LastName); responseDocument.Included[0].Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -257,11 +257,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["description"].Should().Be(newWorkItem.Description); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["assignee"].SingleData.Id.Should().Be(existingUserAccount.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(newWorkItem.Description); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["assignee"].Data.SingleValue.Id.Should().Be(existingUserAccount.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -270,7 +270,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["lastName"].Should().Be(existingUserAccount.LastName); responseDocument.Included[0].Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -298,7 +298,7 @@ public async Task Cannot_create_for_missing_relationship_type() { data = new { - id = 12345678 + id = Unknown.StringId.For() } } } @@ -308,14 +308,14 @@ public async Task Cannot_create_for_missing_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); @@ -336,8 +336,8 @@ public async Task Cannot_create_for_unknown_relationship_type() { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -347,17 +347,17 @@ public async Task Cannot_create_for_unknown_relationship_type() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -385,14 +385,14 @@ public async Task Cannot_create_for_missing_relationship_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); @@ -402,6 +402,8 @@ public async Task Cannot_create_for_missing_relationship_ID() public async Task Cannot_create_with_unknown_relationship_ID() { // Arrange + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new @@ -414,7 +416,7 @@ public async Task Cannot_create_with_unknown_relationship_ID() data = new { type = "userAccounts", - id = 12345678 + id = userAccountId } } } @@ -424,17 +426,17 @@ public async Task Cannot_create_with_unknown_relationship_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '12345678' in relationship 'assignee' does not exist."); + error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); } [Fact] @@ -463,14 +465,14 @@ public async Task Cannot_create_on_relationship_type_mismatch() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -515,7 +517,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string requestBodyText = JsonConvert.SerializeObject(requestBody).Replace("assignee_duplicate", "assignee"); + string requestBodyText = JsonSerializer.Serialize(requestBody).Replace("assignee_duplicate", "assignee"); const string route = "/workItems?include=assignee"; @@ -525,9 +527,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -536,7 +538,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["lastName"].Should().Be(existingUserAccounts[1].LastName); responseDocument.Included[0].Relationships.Should().NotBeEmpty(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -584,14 +586,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); @@ -626,16 +628,16 @@ public async Task Cannot_create_resource_with_local_ID() const string route = "/workItems"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Local IDs cannot be used at this endpoint."); + error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Local IDs cannot be used at this endpoint. - Request body: <<"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs index f3a75b461d..e9d7116889 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs @@ -37,7 +37,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -56,23 +56,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Cannot_delete_missing_resource() + public async Task Cannot_delete_unknown_resource() { // Arrange - const string route = "/workItems/99999999"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -88,7 +90,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/rgbColors/" + existingColor.StringId; + string route = $"/rgbColors/{existingColor.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -123,7 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItemGroups/" + existingGroup.StringId; + string route = $"/workItemGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -159,7 +161,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -195,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs index 5bcb2f98a0..8e89e052d1 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchRelationshipTests.cs @@ -42,11 +42,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("userAccounts"); - responseDocument.SingleData.Id.Should().Be(workItem.Assignee.StringId); - responseDocument.SingleData.Attributes.Should().BeNull(); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("userAccounts"); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.Assignee.StringId); + responseDocument.Data.SingleValue.Attributes.Should().BeNull(); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); } [Fact] @@ -68,7 +68,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -92,14 +92,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); item1.Type.Should().Be("workItems"); item1.Attributes.Should().BeNull(); item1.Relationships.Should().BeNull(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); item2.Type.Should().Be("workItems"); item2.Attributes.Should().BeNull(); item2.Relationships.Should().BeNull(); @@ -125,7 +125,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] @@ -149,14 +149,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); item1.Type.Should().Be("workTags"); item1.Attributes.Should().BeNull(); item1.Relationships.Should().BeNull(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); item2.Type.Should().Be("workTags"); item2.Attributes.Should().BeNull(); item2.Relationships.Should().BeNull(); @@ -182,13 +182,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] public async Task Cannot_get_relationship_for_unknown_primary_type() { - const string route = "/doesNotExist/99999999/relationships/assignee"; + string route = $"/{Unknown.ResourceType}/{Unknown.StringId.Int32}/relationships/assignee"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -202,20 +202,22 @@ public async Task Cannot_get_relationship_for_unknown_primary_type() [Fact] public async Task Cannot_get_relationship_for_unknown_primary_ID() { - const string route = "/workItems/99999999/relationships/assignee"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -229,20 +231,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/workItems/{workItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{workItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs index 9185b53bcb..fbfa714995 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -44,20 +45,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == workItems[0].StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItems[0].StringId); item1.Type.Should().Be("workItems"); item1.Attributes["description"].Should().Be(workItems[0].Description); - item1.Attributes["dueAt"].Should().BeCloseTo(workItems[0].DueAt); - item1.Attributes["priority"].Should().Be(workItems[0].Priority.ToString("G")); + item1.Attributes["dueAt"].As().Should().BeCloseTo(workItems[0].DueAt.GetValueOrDefault()); + item1.Attributes["priority"].Should().Be(workItems[0].Priority); item1.Relationships.Should().NotBeEmpty(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == workItems[1].StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItems[1].StringId); item2.Type.Should().Be("workItems"); item2.Attributes["description"].Should().Be(workItems[1].Description); - item2.Attributes["dueAt"].Should().BeCloseTo(workItems[1].DueAt); - item2.Attributes["priority"].Should().Be(workItems[1].Priority.ToString("G")); + item2.Attributes["dueAt"].As().Should().BeCloseTo(workItems[1].DueAt.GetValueOrDefault()); + item2.Attributes["priority"].Should().Be(workItems[1].Priority); item2.Relationships.Should().NotBeEmpty(); } @@ -65,7 +66,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_primary_resources_for_unknown_type() { // Arrange - const string route = "/doesNotExist"; + const string route = "/" + Unknown.ResourceType; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -88,7 +89,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/workItems/" + workItem.StringId; + string route = $"/workItems/{workItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -96,20 +97,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(workItem.StringId); - responseDocument.SingleData.Attributes["description"].Should().Be(workItem.Description); - responseDocument.SingleData.Attributes["dueAt"].Should().BeCloseTo(workItem.DueAt); - responseDocument.SingleData.Attributes["priority"].Should().Be(workItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.StringId); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(workItem.Description); + responseDocument.Data.SingleValue.Attributes["dueAt"].As().Should().BeCloseTo(workItem.DueAt.GetValueOrDefault()); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(workItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); } [Fact] public async Task Cannot_get_primary_resource_for_unknown_type() { // Arrange - const string route = "/doesNotExist/99999999"; + string route = $"/{Unknown.ResourceType}/{Unknown.StringId.Int32}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -124,20 +125,22 @@ public async Task Cannot_get_primary_resource_for_unknown_type() public async Task Cannot_get_primary_resource_for_unknown_ID() { // Arrange - const string route = "/workItems/99999999"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -161,12 +164,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("userAccounts"); - responseDocument.SingleData.Id.Should().Be(workItem.Assignee.StringId); - responseDocument.SingleData.Attributes["firstName"].Should().Be(workItem.Assignee.FirstName); - responseDocument.SingleData.Attributes["lastName"].Should().Be(workItem.Assignee.LastName); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("userAccounts"); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.Assignee.StringId); + responseDocument.Data.SingleValue.Attributes["firstName"].Should().Be(workItem.Assignee.FirstName); + responseDocument.Data.SingleValue.Attributes["lastName"].Should().Be(workItem.Assignee.LastName); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); } [Fact] @@ -189,7 +192,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -213,20 +216,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(0).StringId); item1.Type.Should().Be("workItems"); item1.Attributes["description"].Should().Be(userAccount.AssignedItems.ElementAt(0).Description); - item1.Attributes["dueAt"].Should().BeCloseTo(userAccount.AssignedItems.ElementAt(0).DueAt); - item1.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(0).Priority.ToString("G")); + item1.Attributes["dueAt"].As().Should().BeCloseTo(userAccount.AssignedItems.ElementAt(0).DueAt.GetValueOrDefault()); + item1.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(0).Priority); item1.Relationships.Should().NotBeEmpty(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == userAccount.AssignedItems.ElementAt(1).StringId); item2.Type.Should().Be("workItems"); item2.Attributes["description"].Should().Be(userAccount.AssignedItems.ElementAt(1).Description); - item2.Attributes["dueAt"].Should().BeCloseTo(userAccount.AssignedItems.ElementAt(1).DueAt); - item2.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(1).Priority.ToString("G")); + item2.Attributes["dueAt"].As().Should().BeCloseTo(userAccount.AssignedItems.ElementAt(1).DueAt.GetValueOrDefault()); + item2.Attributes["priority"].Should().Be(userAccount.AssignedItems.ElementAt(1).Priority); item2.Relationships.Should().NotBeEmpty(); } @@ -250,7 +253,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] @@ -274,15 +277,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - ResourceObject item1 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); + ResourceObject item1 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(0).StringId); item1.Type.Should().Be("workTags"); item1.Attributes["text"].Should().Be(workItem.Tags.ElementAt(0).Text); item1.Attributes["isBuiltIn"].Should().Be(workItem.Tags.ElementAt(0).IsBuiltIn); item1.Relationships.Should().NotBeEmpty(); - ResourceObject item2 = responseDocument.ManyData.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); + ResourceObject item2 = responseDocument.Data.ManyValue.Single(resource => resource.Id == workItem.Tags.ElementAt(1).StringId); item2.Type.Should().Be("workTags"); item2.Attributes["text"].Should().Be(workItem.Tags.ElementAt(1).Text); item2.Attributes["isBuiltIn"].Should().Be(workItem.Tags.ElementAt(1).IsBuiltIn); @@ -309,14 +312,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().BeEmpty(); + responseDocument.Data.ManyValue.Should().BeEmpty(); } [Fact] public async Task Cannot_get_secondary_resource_for_unknown_primary_type() { // Arrange - const string route = "/doesNotExist/99999999/assignee"; + string route = $"/{Unknown.ResourceType}/{Unknown.StringId.Int32}/assignee"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -331,20 +334,22 @@ public async Task Cannot_get_secondary_resource_for_unknown_primary_type() public async Task Cannot_get_secondary_resource_for_unknown_primary_ID() { // Arrange - const string route = "/workItems/99999999/assignee"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -360,20 +365,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/workItems/{workItem.StringId}/doesNotExist"; + string route = $"/workItems/{workItem.StringId}/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs index 81f437dc85..bac9be6cc9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemDefinition.cs @@ -28,7 +28,7 @@ public override async Task OnWriteSucceededAsync(WorkItem resource, WriteOperati { if (writeOperation is not WriteOperationKind.DeleteResource) { - string statement = "Update \"WorkItems\" SET \"Description\" = '" + resource.Description + Suffix + "' WHERE \"Id\" = '" + resource.Id + "'"; + string statement = $"Update \"WorkItems\" SET \"Description\" = '{resource.Description}{Suffix}' WHERE \"Id\" = '{resource.StringId}'"; await _dbContext.Database.ExecuteSqlRawAsync(statement, cancellationToken); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs index 23afe8480b..a5762b761c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/ImplicitlyChangingWorkItemGroupDefinition.cs @@ -29,7 +29,7 @@ public override async Task OnWriteSucceededAsync(WorkItemGroup resource, WriteOp { if (writeOperation is not WriteOperationKind.DeleteResource) { - string statement = "Update \"Groups\" SET \"Name\" = '" + resource.Name + Suffix + "' WHERE \"Id\" = '" + resource.Id + "'"; + string statement = $"Update \"Groups\" SET \"Name\" = '{resource.Name}{Suffix}' WHERE \"Id\" = '{resource.StringId}'"; await _dbContext.Database.ExecuteSqlRawAsync(statement, cancellationToken); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs index d0b1e39b98..d66a425101 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/AddToToManyRelationshipTests.cs @@ -49,14 +49,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Only to-many relationships can be updated through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); @@ -193,14 +193,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -224,7 +224,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - id = 99999999 + id = Unknown.StringId.For() } } }; @@ -232,14 +232,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -263,8 +263,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } }; @@ -272,17 +272,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -311,14 +311,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -336,6 +336,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -343,12 +346,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }; @@ -356,22 +359,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); } [Fact] @@ -386,6 +389,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -393,12 +399,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } }; @@ -406,22 +412,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -449,7 +455,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/subscribers"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/subscribers"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -484,20 +490,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/workItems/99999999/relationships/subscribers"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -519,25 +527,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -568,14 +576,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -687,14 +695,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -720,14 +728,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs index d227ab5e09..ed1f9755ab 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/RemoveFromToManyRelationshipTests.cs @@ -64,14 +64,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Forbidden); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Forbidden); error.Title.Should().Be("Only to-many relationships can be updated through this endpoint."); error.Detail.Should().Be("Relationship 'assignee' must be a to-many relationship."); @@ -309,14 +309,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -349,14 +349,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -380,8 +380,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } }; @@ -389,17 +389,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -428,14 +428,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -453,6 +453,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -460,12 +463,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }; @@ -473,22 +476,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); } [Fact] @@ -503,6 +506,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -510,12 +516,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } }; @@ -523,22 +529,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -566,7 +572,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/subscribers"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/subscribers"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); @@ -601,20 +607,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/workItems/99999999/relationships/subscribers"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -636,25 +644,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -685,14 +693,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -806,14 +814,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -839,14 +847,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs index 8ae734fc0f..3b439c1cec 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/ReplaceToManyRelationshipTests.cs @@ -225,14 +225,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -256,7 +256,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - id = 99999999 + id = Unknown.StringId.For() } } }; @@ -264,14 +264,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -295,8 +295,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } }; @@ -304,17 +304,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -343,14 +343,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -368,6 +368,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -375,12 +378,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }; @@ -388,22 +391,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); } [Fact] @@ -418,6 +421,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new[] @@ -425,12 +431,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } }; @@ -438,22 +444,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(2); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -481,7 +487,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/subscribers"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/subscribers"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -509,20 +515,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = Array.Empty() }; - const string route = "/workItems/99999999/relationships/subscribers"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -544,25 +552,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -593,14 +601,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -679,14 +687,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/subscribers"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -712,14 +720,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/tags"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs index e12ebf1ea4..afe4fc3c97 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Relationships/UpdateToOneRelationshipTests.cs @@ -232,9 +232,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await _testContext.RunOnDatabaseAsync(async dbContext => { - int itemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; + int workItemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; - WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(itemId); + WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(workItemId); workItemInDatabase2.Assignee.Should().NotBeNull(); workItemInDatabase2.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -258,14 +258,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -287,21 +287,21 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = 99999999 + id = Unknown.StringId.For() } }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -323,25 +323,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -367,14 +367,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -392,29 +392,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new { type = "userAccounts", - id = 99999999 + id = userAccountId } }; string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); + error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); } [Fact] @@ -439,7 +441,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/doesNotExist/{existingWorkItem.StringId}/relationships/assignee"; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}/relationships/assignee"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -471,20 +473,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/workItems/99999999/relationships/assignee"; + string workItemId = Unknown.StringId.For(); + + string route = $"/workItems/{workItemId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -504,24 +508,24 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "userAccounts", - id = 99999999 + id = Unknown.StringId.For() } }; - string route = $"/workItems/{existingWorkItem.StringId}/relationships/doesNotExist"; + string route = $"/workItems/{existingWorkItem.StringId}/relationships/{Unknown.Relationship}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested relationship does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' does not contain a relationship named 'doesNotExist'."); + error.Detail.Should().Be($"Resource of type 'workItems' does not contain a relationship named '{Unknown.Relationship}'."); } [Fact] @@ -549,14 +553,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -592,14 +596,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/workItems/{existingWorkItem.StringId}/relationships/assignee"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs index 19d0979648..b225589c4e 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/ReplaceToManyRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -68,7 +68,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -107,7 +107,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -115,7 +115,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -168,7 +168,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -176,7 +176,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -237,7 +237,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -245,7 +245,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -302,11 +302,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -368,14 +368,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData[0].Id.Should().Be(existingTag.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[0].Id.Should().Be(existingTag.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("workTags"); @@ -384,7 +384,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Included[0].Attributes["text"].Should().Be(existingTag.Text); responseDocument.Included[0].Relationships.Should().BeNull(); - int newWorkItemId = int.Parse(responseDocument.SingleData.Id); + int newWorkItemId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -421,7 +421,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - id = 99999999 + id = Unknown.StringId.For() } } } @@ -429,17 +429,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'subscribers' relationship. - Request body: <<"); @@ -471,8 +471,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } @@ -480,20 +480,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -530,17 +530,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'subscribers' relationship. - Request body: <<"); @@ -558,6 +558,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId1 = Unknown.StringId.For(); + string userAccountId2 = Unknown.StringId.AltFor(); + + string tagId1 = Unknown.StringId.For(); + string tagId2 = Unknown.StringId.AltFor(); + var requestBody = new { data = new @@ -573,12 +579,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "userAccounts", - id = 88888888 + id = userAccountId1 }, new { type = "userAccounts", - id = 99999999 + id = userAccountId2 } } }, @@ -589,12 +595,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "workTags", - id = 88888888 + id = tagId1 }, new { type = "workTags", - id = 99999999 + id = tagId2 } } } @@ -602,35 +608,35 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(4); - Error error1 = responseDocument.Errors[0]; + ErrorObject error1 = responseDocument.Errors[0]; error1.StatusCode.Should().Be(HttpStatusCode.NotFound); error1.Title.Should().Be("A related resource does not exist."); - error1.Detail.Should().Be("Related resource of type 'userAccounts' with ID '88888888' in relationship 'subscribers' does not exist."); + error1.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId1}' in relationship 'subscribers' does not exist."); - Error error2 = responseDocument.Errors[1]; + ErrorObject error2 = responseDocument.Errors[1]; error2.StatusCode.Should().Be(HttpStatusCode.NotFound); error2.Title.Should().Be("A related resource does not exist."); - error2.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'subscribers' does not exist."); + error2.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId2}' in relationship 'subscribers' does not exist."); - Error error3 = responseDocument.Errors[2]; + ErrorObject error3 = responseDocument.Errors[2]; error3.StatusCode.Should().Be(HttpStatusCode.NotFound); error3.Title.Should().Be("A related resource does not exist."); - error3.Detail.Should().Be("Related resource of type 'workTags' with ID '88888888' in relationship 'tags' does not exist."); + error3.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId1}' in relationship 'tags' does not exist."); - Error error4 = responseDocument.Errors[3]; + ErrorObject error4 = responseDocument.Errors[3]; error4.StatusCode.Should().Be(HttpStatusCode.NotFound); error4.Title.Should().Be("A related resource does not exist."); - error4.Detail.Should().Be("Related resource of type 'workTags' with ID '99999999' in relationship 'tags' does not exist."); + error4.Detail.Should().Be($"Related resource of type 'workTags' with ID '{tagId2}' in relationship 'tags' does not exist."); } [Fact] @@ -668,17 +674,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'subscribers' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -727,7 +733,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -735,7 +741,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -774,17 +780,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'subscribers' relationship. - Request body: <<"); @@ -818,17 +824,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected data[] element for to-many relationship."); error.Detail.Should().StartWith("Expected data[] element for 'tags' relationship. - Request body: <<"); @@ -865,7 +871,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -873,7 +879,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -914,7 +920,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -922,7 +928,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -977,7 +983,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -985,7 +991,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1031,7 +1037,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1039,7 +1045,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs index aea4a3734b..a3bf1986f2 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateResourceTests.cs @@ -63,7 +63,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -152,15 +152,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 12345678 + type = Unknown.ResourceType, + id = Unknown.StringId.Int32 } } } } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -197,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItemGroups/" + existingGroup.StringId; + string route = $"/workItemGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -205,18 +205,18 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItemGroups"); - responseDocument.SingleData.Id.Should().Be(existingGroup.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(newName + ImplicitlyChangingWorkItemGroupDefinition.Suffix); - responseDocument.SingleData.Attributes["isPublic"].Should().Be(existingGroup.IsPublic); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItemGroups"); + responseDocument.Data.SingleValue.Id.Should().Be(existingGroup.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be($"{newName}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["isPublic"].Should().Be(existingGroup.IsPublic); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItemGroup groupInDatabase = await dbContext.Groups.FirstWithIdAsync(existingGroup.Id); - groupInDatabase.Name.Should().Be(newName + ImplicitlyChangingWorkItemGroupDefinition.Suffix); + groupInDatabase.Name.Should().Be($"{newName}{ImplicitlyChangingWorkItemGroupDefinition.Suffix}"); groupInDatabase.IsPublic.Should().Be(existingGroup.IsPublic); }); @@ -250,7 +250,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/rgbColors/" + existingColor.StringId; + string route = $"/rgbColors/{existingColor.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -298,7 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/userAccounts/" + existingUserAccount.StringId; + string route = $"/userAccounts/{existingUserAccount.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -344,7 +344,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -352,20 +352,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Attributes["dueAt"].Should().BeNull(); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Attributes["isImportant"].Should().Be(existingWorkItem.IsImportant); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["dueAt"].Should().BeNull(); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Attributes["isImportant"].Should().Be(existingWorkItem.IsImportant); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItem workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.DueAt.Should().BeNull(); workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); }); @@ -406,19 +406,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItem workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.DueAt.Should().BeNull(); workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); }); @@ -461,15 +461,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Attributes["priority"].Should().Be(existingWorkItem.Priority.ToString("G")); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData.Should().HaveCount(1); - responseDocument.SingleData.Relationships["tags"].ManyData[0].Id.Should().Be(existingWorkItem.Tags.Single().StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Attributes["priority"].Should().Be(existingWorkItem.Priority); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["tags"].Data.ManyValue[0].Id.Should().Be(existingWorkItem.Tags.Single().StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("workTags"); @@ -482,7 +482,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { WorkItem workItemInDatabase = await dbContext.WorkItems.FirstWithIdAsync(existingWorkItem.Id); - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.DueAt.Should().BeNull(); workItemInDatabase.Priority.Should().Be(existingWorkItem.Priority); }); @@ -510,7 +510,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -518,9 +518,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); - responseDocument.SingleData.Relationships.Values.Should().OnlyContain(relationshipEntry => relationshipEntry.Data == null); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Relationships.Values.Should().OnlyContain(relationshipObject => relationshipObject.Data.Value == null); responseDocument.Included.Should().BeNull(); } @@ -539,17 +539,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string requestBody = string.Empty; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Missing request body."); error.Detail.Should().BeNull(); @@ -575,17 +575,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'data' element. - Request body: <<"); @@ -607,25 +607,25 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", + type = Unknown.ResourceType, id = existingWorkItem.StringId } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -648,17 +648,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Request body: <<"); @@ -685,7 +685,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/doesNotExist/" + existingWorkItem.StringId; + string route = $"/{Unknown.ResourceType}/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -700,29 +700,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_update_resource_on_unknown_resource_ID_in_url() { // Arrange + string workItemId = Unknown.StringId.For(); + var requestBody = new { data = new { type = "workItems", - id = 99999999 + id = workItemId } }; - const string route = "/workItems/99999999"; + string route = $"/workItems/{workItemId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'workItems' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'workItems' with ID '{workItemId}' does not exist."); } [Fact] @@ -746,17 +748,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource type mismatch between request body and endpoint URL."); @@ -785,17 +787,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItems[1].StringId; + string route = $"/workItems/{existingWorkItems[1].StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.Conflict); error.Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); @@ -828,20 +830,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); - error.Detail.Should().StartWith("Changing the value of 'isImportant' is not allowed. - Request body:"); + error.Detail.Should().StartWith("Changing the value of 'isImportant' is not allowed. - Request body: <<"); } [Fact] @@ -869,20 +871,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItemGroups/" + existingWorkItem.StringId; + string route = $"/workItemGroups/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Attribute is read-only."); - error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body:"); + error.Detail.Should().StartWith("Attribute 'isDeprecated' is read-only. - Request body: <<"); } [Fact] @@ -897,22 +899,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - const string requestBody = "{ \"data\" {"; + const string requestBody = "{ \"data {"; - string route = "/workItemGroups/" + existingWorkItem.StringId; + string route = $"/workItemGroups/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Invalid character after parsing"); + error.Detail.Should().Match("Expected end of string, but instead reached end of data. * - Request body: <<*"); } [Fact] @@ -935,27 +937,67 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingWorkItem.StringId, attributes = new { - id = existingWorkItem.Id + 123456 + id = Unknown.StringId.For() } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); - error.Title.Should().Be("Failed to deserialize request body: Resource ID is read-only."); + error.Title.Should().Be("Failed to deserialize request body."); error.Detail.Should().StartWith("Resource ID is read-only. - Request body: <<"); } + [Fact] + public async Task Cannot_update_resource_with_incompatible_ID_value() + { + // Arrange + WorkItem existingWorkItem = _fakers.WorkItem.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.WorkItems.Add(existingWorkItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "workItems", + id = existingWorkItem.Id, + attributes = new + { + } + } + }; + + string route = $"/workItems/{existingWorkItem.StringId}"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + + ErrorObject error = responseDocument.Errors[0]; + error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + error.Title.Should().Be("Failed to deserialize request body."); + error.Detail.Should().StartWith($"Failed to convert ID '{existingWorkItem.Id}' of type 'Number' to type 'String'. - Request body: <<"); + } + [Fact] public async Task Cannot_update_resource_with_incompatible_attribute_value() { @@ -976,25 +1018,31 @@ await _testContext.RunOnDatabaseAsync(async dbContext => id = existingWorkItem.StringId, attributes = new { - dueAt = "not-a-valid-time" + dueAt = new + { + Start = 10, + End = 20 + } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body."); - error.Detail.Should().StartWith("Failed to convert 'not-a-valid-time' of type 'String' to type 'Nullable`1'. - Request body: <<"); + + error.Detail.Should().Match("Failed to convert attribute 'dueAt' with value '*start*end*' " + + "of type 'Object' to type 'Nullable'. - Request body: <<*"); } [Fact] @@ -1064,7 +1112,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1072,9 +1120,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["description"].Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -1090,7 +1138,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // @formatter:keep_existing_linebreaks restore // @formatter:wrap_chained_method_calls restore - workItemInDatabase.Description.Should().Be(newDescription + ImplicitlyChangingWorkItemDefinition.Suffix); + workItemInDatabase.Description.Should().Be($"{newDescription}{ImplicitlyChangingWorkItemDefinition.Suffix}"); workItemInDatabase.Assignee.Should().NotBeNull(); workItemInDatabase.Assignee.Id.Should().Be(existingUserAccounts[0].Id); @@ -1160,7 +1208,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1168,7 +1216,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs index ded40750a3..3eac83cc04 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Updating/Resources/UpdateToOneRelationshipTests.cs @@ -60,7 +60,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -68,7 +68,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -113,7 +113,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItemGroups/" + existingGroup.StringId; + string route = $"/workItemGroups/{existingGroup.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -169,7 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/rgbColors/" + existingGroups[0].Color.StringId; + string route = $"/rgbColors/{existingGroups[0].Color.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -231,7 +231,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/rgbColors/" + existingColor.StringId; + string route = $"/rgbColors/{existingColor.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -282,20 +282,20 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingUserAccounts[0].AssignedItems.ElementAt(1).StringId; + string route = $"/workItems/{existingUserAccounts[0].AssignedItems.ElementAt(1).StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { - int itemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; + int workItemId = existingUserAccounts[0].AssignedItems.ElementAt(1).Id; - WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(itemId); + WorkItem workItemInDatabase2 = await dbContext.WorkItems.Include(workItem => workItem.Assignee).FirstWithIdAsync(workItemId); workItemInDatabase2.Assignee.Should().NotBeNull(); workItemInDatabase2.Assignee.Id.Should().Be(existingUserAccounts[1].Id); @@ -343,11 +343,13 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes["description"].Should().Be(existingWorkItem.Description + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().NotBeEmpty(); + string description = $"{existingWorkItem.Description}{ImplicitlyChangingWorkItemDefinition.Suffix}"; + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(description); + responseDocument.Data.SingleValue.Relationships.Should().NotBeEmpty(); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -408,13 +410,15 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("workItems"); - responseDocument.SingleData.Id.Should().Be(existingWorkItem.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["description"].Should().Be(existingWorkItem.Description + ImplicitlyChangingWorkItemDefinition.Suffix); - responseDocument.SingleData.Relationships.Should().HaveCount(1); - responseDocument.SingleData.Relationships["assignee"].SingleData.Id.Should().Be(existingUserAccount.StringId); + string description = $"{existingWorkItem.Description}{ImplicitlyChangingWorkItemDefinition.Suffix}"; + + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("workItems"); + responseDocument.Data.SingleValue.Id.Should().Be(existingWorkItem.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["description"].Should().Be(description); + responseDocument.Data.SingleValue.Relationships.Should().HaveCount(1); + responseDocument.Data.SingleValue.Relationships["assignee"].Data.SingleValue.Id.Should().Be(existingUserAccount.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("userAccounts"); @@ -456,24 +460,24 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = 99999999 + id = Unknown.StringId.For() } } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'type' element."); error.Detail.Should().StartWith("Expected 'type' element in 'assignee' relationship. - Request body: <<"); @@ -503,28 +507,28 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - type = "doesNotExist", - id = 99999999 + type = Unknown.ResourceType, + id = Unknown.StringId.For() } } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body includes unknown resource type."); - error.Detail.Should().StartWith("Resource type 'doesNotExist' does not exist. - Request body: <<"); + error.Detail.Should().StartWith($"Resource type '{Unknown.ResourceType}' does not exist. - Request body: <<"); } [Fact] @@ -558,17 +562,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Request body must include 'id' element."); error.Detail.Should().StartWith("Expected 'id' element in 'assignee' relationship. - Request body: <<"); @@ -586,6 +590,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); + string userAccountId = Unknown.StringId.For(); + var requestBody = new { data = new @@ -599,27 +605,27 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "userAccounts", - id = 99999999 + id = userAccountId } } } } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); - error.Detail.Should().Be("Related resource of type 'userAccounts' with ID '99999999' in relationship 'assignee' does not exist."); + error.Detail.Should().Be($"Related resource of type 'userAccounts' with ID '{userAccountId}' in relationship 'assignee' does not exist."); } [Fact] @@ -654,17 +660,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Relationship contains incompatible resource type."); error.Detail.Should().StartWith("Relationship 'assignee' contains incompatible resource type 'rgbColors'. - Request body: <<"); @@ -706,17 +712,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); error.Title.Should().Be("Failed to deserialize request body: Expected single data element for to-one relationship."); error.Detail.Should().StartWith("Expected single data element for 'assignee' relationship. - Request body: <<"); @@ -753,7 +759,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -761,7 +767,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -803,7 +809,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/workItems/" + existingWorkItem.StringId; + string route = $"/workItems/{existingWorkItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -811,7 +817,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs index 093a2d5a86..e7a5a113bf 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/WorkItemPriority.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.ReadWrite { [UsedImplicitly(ImplicitUseTargetFlags.Members)] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum WorkItemPriority { Low, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs index 140065daed..20448ea5db 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RequiredRelationships/DefaultBehaviorTests.cs @@ -51,14 +51,14 @@ public async Task Cannot_create_dependent_side_of_required_ManyToOne_relationshi const string route = "/orders"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Failed to persist changes in the underlying data store."); @@ -85,14 +85,14 @@ public async Task Cannot_create_dependent_side_of_required_OneToOne_relationship const string route = "/shipments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.InternalServerError); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.InternalServerError); error.Title.Should().Be("An unhandled error occurred while processing this request."); error.Detail.Should().Be("Failed to persist changes in the underlying data store."); @@ -111,7 +111,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/customers/{existingOrder.Customer.Id}"; + string route = $"/customers/{existingOrder.Customer.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -145,7 +145,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = $"/orders/{existingOrder.Id}"; + string route = $"/orders/{existingOrder.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -186,7 +186,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = existingOrder.Id, + id = existingOrder.StringId, type = "orders", relationships = new { @@ -198,17 +198,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/orders/{existingOrder.Id}"; + string route = $"/orders/{existingOrder.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -235,17 +235,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = (object)null }; - string route = $"/orders/{existingOrder.Id}/relationships/customer"; + string route = $"/orders/{existingOrder.StringId}/relationships/customer"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -271,7 +271,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = existingOrder.Customer.Id, + id = existingOrder.Customer.StringId, type = "customers", relationships = new { @@ -283,17 +283,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/customers/{existingOrder.Customer.Id}"; + string route = $"/customers/{existingOrder.Customer.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -320,17 +320,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = Array.Empty() }; - string route = $"/customers/{existingOrder.Customer.Id}/relationships/orders"; + string route = $"/customers/{existingOrder.Customer.StringId}/relationships/orders"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -359,22 +359,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "orders", - id = existingOrder.Id + id = existingOrder.StringId } } }; - string route = $"/customers/{existingOrder.Customer.Id}/relationships/orders"; + string route = $"/customers/{existingOrder.Customer.StringId}/relationships/orders"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Failed to clear a required relationship."); @@ -403,7 +403,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = orderWithoutShipment.Id, + id = orderWithoutShipment.StringId, type = "orders", relationships = new { @@ -411,7 +411,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = orderWithShipment.Shipment.Id, + id = orderWithShipment.Shipment.StringId, type = "shipments" } } @@ -419,7 +419,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/orders/{orderWithoutShipment.Id}"; + string route = $"/orders/{orderWithoutShipment.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -459,12 +459,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => { data = new { - id = orderWithShipment.Shipment.Id, + id = orderWithShipment.Shipment.StringId, type = "shipments" } }; - string route = $"/orders/{orderWithoutShipment.Id}/relationships/shipment"; + string route = $"/orders/{orderWithoutShipment.StringId}/relationships/shipment"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs index c08840a5d6..97716b7812 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection/ResourceInjectionTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Net; using System.Net.Http; @@ -50,7 +51,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/giftCertificates/" + certificate.StringId; + string route = $"/giftCertificates/{certificate.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -58,10 +59,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(certificate.StringId); - responseDocument.SingleData.Attributes["issueDate"].Should().BeCloseTo(certificate.IssueDate); - responseDocument.SingleData.Attributes["hasExpired"].Should().Be(false); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(certificate.StringId); + responseDocument.Data.SingleValue.Attributes["issueDate"].As().Should().BeCloseTo(certificate.IssueDate); + responseDocument.Data.SingleValue.Attributes["hasExpired"].Should().Be(false); } [Fact] @@ -88,10 +89,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(postOffices[1].StringId); - responseDocument.ManyData[0].Attributes["address"].Should().Be(postOffices[1].Address); - responseDocument.ManyData[0].Attributes["isOpen"].Should().Be(true); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(postOffices[1].StringId); + responseDocument.Data.ManyValue[0].Attributes["address"].Should().Be(postOffices[1].Address); + responseDocument.Data.ManyValue[0].Attributes["isOpen"].Should().Be(true); } [Fact] @@ -118,10 +119,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(certificate.Issuer.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["isOpen"].Should().Be(true); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(certificate.Issuer.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["isOpen"].Should().Be(true); } [Fact] @@ -172,17 +173,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["issueDate"].Should().BeCloseTo(newIssueDate); - responseDocument.SingleData.Attributes["hasExpired"].Should().Be(true); - responseDocument.SingleData.Relationships["issuer"].SingleData.Id.Should().Be(existingOffice.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["issueDate"].As().Should().BeCloseTo(newIssueDate); + responseDocument.Data.SingleValue.Attributes["hasExpired"].Should().Be(true); + responseDocument.Data.SingleValue.Relationships["issuer"].Data.SingleValue.Id.Should().Be(existingOffice.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(existingOffice.StringId); responseDocument.Included[0].Attributes["address"].Should().Be(existingOffice.Address); responseDocument.Included[0].Attributes["isOpen"].Should().Be(false); - int newCertificateId = int.Parse(responseDocument.SingleData.Id); + int newCertificateId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -241,7 +242,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/postOffices/" + existingOffice.StringId; + string route = $"/postOffices/{existingOffice.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -274,7 +275,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/postOffices/" + existingOffice.StringId; + string route = $"/postOffices/{existingOffice.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -296,20 +297,22 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_delete_unknown_resource() { // Arrange - const string route = "/postOffices/99999999"; + string officeId = Unknown.StringId.For(); + + string route = $"/postOffices/{officeId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); - error.Detail.Should().Be("Resource of type 'postOffices' with ID '99999999' does not exist."); + error.Detail.Should().Be($"Resource of type 'postOffices' with ID '{officeId}' does not exist."); } [Fact] diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs index 5cdf8d49fd..a40696a23a 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/MoonDefinition.cs @@ -35,8 +35,7 @@ public override IImmutableList OnApplyIncludes(IImmuta return existingIncludes; } - RelationshipAttribute orbitsAroundRelationship = - ResourceContext.Relationships.Single(relationship => relationship.Property.Name == nameof(Moon.OrbitsAround)); + RelationshipAttribute orbitsAroundRelationship = ResourceContext.GetRelationshipByPropertyName(nameof(Moon.OrbitsAround)); return existingIncludes.Add(new IncludeElementExpression(orbitsAroundRelationship)); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs index e121087275..b2313bee98 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/PlanetDefinition.cs @@ -34,7 +34,7 @@ public override IImmutableList OnApplyIncludes(IImmuta if (_clientSettingsProvider.IsIncludePlanetMoonsBlocked && existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Planet.Moons))) { - throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest) { Title = "Including moons is not permitted." }); @@ -49,7 +49,7 @@ public override FilterExpression OnApplyFilter(FilterExpression existingFilter) if (_clientSettingsProvider.ArePlanetsWithPrivateNameHidden) { - AttrAttribute privateNameAttribute = ResourceContext.Attributes.Single(attribute => attribute.Property.Name == nameof(Planet.PrivateName)); + AttrAttribute privateNameAttribute = ResourceContext.GetAttributeByPropertyName(nameof(Planet.PrivateName)); FilterExpression hasNoPrivateName = new ComparisonExpression(ComparisonOperator.Equals, new ResourceFieldChainExpression(privateNameAttribute), new NullConstantExpression()); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs index 0cd6d121d4..675d562f5f 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/ResourceDefinitionReadTests.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; @@ -66,14 +67,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/planets?include=moons"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Including moons is not permitted."); error.Detail.Should().BeNull(); @@ -104,7 +105,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/moons/" + moon.StringId; + string route = $"/moons/{moon.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -112,9 +113,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Type.Should().Be("planets"); - responseDocument.SingleData.Relationships["orbitsAround"].SingleData.Id.Should().Be(moon.OrbitsAround.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Relationships["orbitsAround"].Data.SingleValue.Type.Should().Be("planets"); + responseDocument.Data.SingleValue.Relationships["orbitsAround"].Data.SingleValue.Id.Should().Be(moon.OrbitsAround.StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("planets"); @@ -155,11 +156,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(planets[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(planets[3].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(planets[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(planets[3].StringId); - responseDocument.Meta["totalResources"].Should().Be(2); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(2); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -205,10 +206,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(planets[3].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(planets[3].StringId); - responseDocument.Meta["totalResources"].Should().Be(1); + ((JsonElement)responseDocument.Meta["total"]).GetInt32().Should().Be(1); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -250,10 +251,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(stars[1].StringId); - responseDocument.ManyData[1].Id.Should().Be(stars[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(stars[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(stars[1].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(stars[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(stars[2].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -297,10 +298,10 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(3); - responseDocument.ManyData[0].Id.Should().Be(stars[2].StringId); - responseDocument.ManyData[1].Id.Should().Be(stars[0].StringId); - responseDocument.ManyData[2].Id.Should().Be(stars[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(3); + responseDocument.Data.ManyValue[0].Id.Should().Be(stars[2].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(stars[0].StringId); + responseDocument.Data.ManyValue[2].Id.Should().Be(stars[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -335,7 +336,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(5); + responseDocument.Data.ManyValue.Should().HaveCount(5); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -369,11 +370,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Attributes["kind"].Should().Be(star.Kind.ToString()); - responseDocument.SingleData.Relationships.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes["kind"].Should().Be(star.Kind); + responseDocument.Data.SingleValue.Relationships.Should().NotBeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -407,12 +408,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(2); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Attributes["solarRadius"].As().Should().BeApproximately(star.SolarRadius); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(2); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes["solarRadius"].As().Should().BeApproximately(star.SolarRadius); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -446,11 +447,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Attributes.Should().NotContainKey("isVisibleFromEarth"); - responseDocument.SingleData.Relationships.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Attributes.Should().NotContainKey("isVisibleFromEarth"); + responseDocument.Data.SingleValue.Relationships.Should().NotBeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -484,11 +485,11 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(star.StringId); - responseDocument.SingleData.Attributes.Should().HaveCount(1); - responseDocument.SingleData.Attributes["name"].Should().Be(star.Name); - responseDocument.SingleData.Relationships.Should().BeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(star.StringId); + responseDocument.Data.SingleValue.Attributes.Should().HaveCount(1); + responseDocument.Data.SingleValue.Attributes["name"].Should().Be(star.Name); + responseDocument.Data.SingleValue.Relationships.Should().BeNull(); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -526,8 +527,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(moons[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(moons[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -572,8 +573,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(moons[2].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(moons[2].StringId); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] { @@ -601,14 +602,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/planets/{planet.StringId}/moons?isLargerThanTheSun=false"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Custom query string parameters cannot be used on nested resource endpoints."); error.Detail.Should().Be("Query string parameter 'isLargerThanTheSun' cannot be used on a nested resource endpoint."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs index 7a04056716..d3c4685d1c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Reading/StarKind.cs @@ -1,8 +1,10 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceDefinitions.Reading { [UsedImplicitly(ImplicitUseTargetFlags.Members)] + [JsonConverter(typeof(JsonStringEnumMemberConverter))] public enum StarKind { Other, diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs index 693907a79a..216de1a998 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceDefinitions/Serialization/ResourceDefinitionSerializationTests.cs @@ -63,12 +63,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.ManyData[0].Attributes["socialSecurityNumber"]); + string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[0].Attributes["socialSecurityNumber"]); socialSecurityNumber1.Should().Be(students[0].SocialSecurityNumber); - string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.ManyData[1].Attributes["socialSecurityNumber"]); + string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(students[1].SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -104,7 +104,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); responseDocument.Included.Should().HaveCount(4); @@ -144,7 +144,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/students/" + student.StringId; + string route = $"/students/{student.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -152,9 +152,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(student.SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -187,12 +187,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); + responseDocument.Data.ManyValue.Should().HaveCount(2); - string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.ManyData[0].Attributes["socialSecurityNumber"]); + string socialSecurityNumber1 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[0].Attributes["socialSecurityNumber"]); socialSecurityNumber1.Should().Be(scholarship.Participants[0].SocialSecurityNumber); - string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.ManyData[1].Attributes["socialSecurityNumber"]); + string socialSecurityNumber2 = encryptionService.Decrypt((string)responseDocument.Data.ManyValue[1].Attributes["socialSecurityNumber"]); socialSecurityNumber2.Should().Be(scholarship.Participants[1].SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -226,9 +226,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(scholarship.PrimaryContact.SocialSecurityNumber); hitCounter.HitExtensibilityPoints.Should().BeEquivalentTo(new[] @@ -261,7 +261,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); @@ -305,12 +305,12 @@ public async Task Decrypts_on_create_resource() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(newSocialSecurityNumber); - int newStudentId = int.Parse(responseDocument.SingleData.Id); + int newStudentId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -376,7 +376,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(1); @@ -419,7 +419,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/students/" + existingStudent.StringId; + string route = $"/students/{existingStudent.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -427,9 +427,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); - string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.SingleData.Attributes["socialSecurityNumber"]); + string socialSecurityNumber = encryptionService.Decrypt((string)responseDocument.Data.SingleValue.Attributes["socialSecurityNumber"]); socialSecurityNumber.Should().Be(newSocialSecurityNumber); await _testContext.RunOnDatabaseAsync(async dbContext => @@ -504,7 +504,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); + responseDocument.Data.SingleValue.Should().NotBeNull(); responseDocument.Included.Should().HaveCount(2); @@ -544,8 +544,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(scholarship.PrimaryContact.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(scholarship.PrimaryContact.StringId); hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } @@ -573,9 +573,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(2); - responseDocument.ManyData[0].Id.Should().Be(scholarship.Participants[0].StringId); - responseDocument.ManyData[1].Id.Should().Be(scholarship.Participants[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(2); + responseDocument.Data.ManyValue[0].Id.Should().Be(scholarship.Participants[0].StringId); + responseDocument.Data.ManyValue[1].Id.Should().Be(scholarship.Participants[1].StringId); hitCounter.HitExtensibilityPoints.Should().BeEmpty(); } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs index be72317118..477b450167 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/InheritanceTests.cs @@ -54,13 +54,13 @@ public async Task Can_create_resource_with_inherited_attributes() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Type.Should().Be("men"); - responseDocument.SingleData.Attributes["familyName"].Should().Be(newMan.FamilyName); - responseDocument.SingleData.Attributes["isRetired"].Should().Be(newMan.IsRetired); - responseDocument.SingleData.Attributes["hasBeard"].Should().Be(newMan.HasBeard); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("men"); + responseDocument.Data.SingleValue.Attributes["familyName"].Should().Be(newMan.FamilyName); + responseDocument.Data.SingleValue.Attributes["isRetired"].Should().Be(newMan.IsRetired); + responseDocument.Data.SingleValue.Attributes["hasBeard"].Should().Be(newMan.HasBeard); - int newManId = int.Parse(responseDocument.SingleData.Id); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -112,8 +112,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - int newManId = int.Parse(responseDocument.SingleData.Id); + responseDocument.Data.SingleValue.Should().NotBeNull(); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -163,7 +163,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/men/" + existingMan.StringId; + string route = $"/men/{existingMan.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -274,8 +274,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - int newManId = int.Parse(responseDocument.SingleData.Id); + responseDocument.Data.SingleValue.Should().NotBeNull(); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -388,8 +388,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - int newManId = int.Parse(responseDocument.SingleData.Id); + responseDocument.Data.SingleValue.Should().NotBeNull(); + int newManId = int.Parse(responseDocument.Data.SingleValue.Id); await _testContext.RunOnDatabaseAsync(async dbContext => { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs index e99ee891f8..54356a48f7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/DisableQueryStringTests.cs @@ -35,14 +35,14 @@ public async Task Cannot_sort_if_query_string_parameter_is_blocked_by_controller const string route = "/sofas?sort=id"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'sort' cannot be used at this endpoint."); @@ -56,14 +56,14 @@ public async Task Cannot_paginate_if_query_string_parameter_is_blocked_by_contro const string route = "/sofas?page[number]=2"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'page[number]' cannot be used at this endpoint."); @@ -77,14 +77,14 @@ public async Task Cannot_use_custom_query_string_parameter_if_blocked_by_control const string route = "/beds?skipCache=true"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.BadRequest); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.BadRequest); error.Title.Should().Be("Usage of one or more query string parameters is not allowed at the requested endpoint."); error.Detail.Should().Be("The parameter 'skipCache' cannot be used at this endpoint."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs index b8894c852e..8a307b1de7 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/HttpReadOnlyTests.cs @@ -51,14 +51,14 @@ public async Task Cannot_create_resource() const string route = "/beds"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support POST requests."); @@ -88,17 +88,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/beds/" + existingBed.StringId; + string route = $"/beds/{existingBed.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support PATCH requests."); @@ -116,17 +116,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/beds/" + existingBed.StringId; + string route = $"/beds/{existingBed.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support DELETE requests."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs index 88917b3fd8..fb7ceadc45 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpDeleteTests.cs @@ -81,7 +81,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/sofas/" + existingSofa.StringId; + string route = $"/sofas/{existingSofa.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -102,17 +102,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/sofas/" + existingSofa.StringId; + string route = $"/sofas/{existingSofa.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support DELETE requests."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs index 1fbcbedade..5be0254212 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPatchTests.cs @@ -81,17 +81,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/chairs/" + existingChair.StringId; + string route = $"/chairs/{existingChair.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support PATCH requests."); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/chairs/" + existingChair.StringId; + string route = $"/chairs/{existingChair.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs index b9dde5217c..a6f16a1cb0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/RestrictedControllers/NoHttpPostTests.cs @@ -51,14 +51,14 @@ public async Task Cannot_create_resource() const string route = "/tables"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.MethodNotAllowed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.MethodNotAllowed); error.Title.Should().Be("The request method is not allowed."); error.Detail.Should().Be("Endpoint does not support POST requests."); @@ -88,7 +88,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/tables/" + existingTable.StringId; + string route = $"/tables/{existingTable.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -109,7 +109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/tables/" + existingTable.StringId; + string route = $"/tables/{existingTable.StringId}"; // Act (HttpResponseMessage httpResponse, _) = await _testContext.ExecuteDeleteAsync(route); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs index ebf88cda99..8763390bd9 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/ETagTests.cs @@ -84,7 +84,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Returns_no_ETag_for_failed_GET_request() { // Arrange - const string route = "/meetings/99999999"; + string route = $"/meetings/{Unknown.StringId.For()}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -155,7 +155,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/meetings/" + existingMeeting.StringId; + string route = $"/meetings/{existingMeeting.StringId}"; Action setRequestHeaders = headers => { @@ -163,18 +163,19 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = - await _testContext.ExecutePatchAsync(route, requestBody, setRequestHeaders: setRequestHeaders); + (HttpResponseMessage httpResponse, Document responseDocument) = + await _testContext.ExecutePatchAsync(route, requestBody, setRequestHeaders: setRequestHeaders); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.PreconditionFailed); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.PreconditionFailed); error.Title.Should().Be("Detection of mid-air edit collisions using ETags is not supported."); error.Detail.Should().BeNull(); + error.Source.Header.Should().Be("If-Match"); } [Fact] @@ -198,7 +199,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => Action setRequestHeaders2 = headers => { - headers.IfNoneMatch.ParseAdd("\"12345\", W/\"67890\", " + responseETag); + headers.IfNoneMatch.ParseAdd($"\"12345\", W/\"67890\", {responseETag}"); }; // Act diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs index 0d48615e25..8ca431bc1c 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/MeetingLocation.cs @@ -1,13 +1,15 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class MeetingLocation { - [JsonProperty("lat")] + [JsonPropertyName("lat")] public double Latitude { get; set; } - [JsonProperty("lng")] + [JsonPropertyName("lng")] public double Longitude { get; set; } } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs index 95524efea0..f73e03004d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Serialization/SerializationTests.cs @@ -4,13 +4,12 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json.Serialization; using System.Threading.Tasks; using FluentAssertions; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using TestBuildingBlocks; using Xunit; @@ -18,6 +17,8 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.Serialization { public sealed class SerializationTests : IClassFixture, SerializationDbContext>> { + private const string JsonDateTimeOffsetFormatSpecifier = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK"; + private readonly IntegrationTestContext, SerializationDbContext> _testContext; private readonly SerializationFakers _fakers = new(); @@ -37,6 +38,11 @@ public SerializationTests(IntegrationTestContext converter is JsonTimeSpanConverter)) + { + options.SerializerOptions.Converters.Add(new JsonTimeSpanConverter()); + } } [Fact] @@ -51,7 +57,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/meetings/" + meeting.StringId; + string route = $"/meetings/{meeting.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteHeadAsync(route); @@ -66,7 +72,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Returns_no_body_for_failed_HEAD_request() { // Arrange - const string route = "/meetings/99999999"; + string route = $"/meetings/{Unknown.StringId.For()}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteHeadAsync(route); @@ -110,7 +116,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + meetings[0].StringId + @""", ""attributes"": { ""title"": """ + meetings[0].Title + @""", - ""startTime"": """ + meetings[0].StartTime.ToString("O") + @""", + ""startTime"": """ + meetings[0].StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + meetings[0].Duration + @""", ""location"": { ""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -191,7 +197,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + meetings[0].StringId + @""", ""attributes"": { ""title"": """ + meetings[0].Title + @""", - ""startTime"": """ + meetings[0].StartTime.ToString("O") + @""", + ""startTime"": """ + meetings[0].StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + meetings[0].Duration + @""", ""location"": { ""lat"": " + meetings[0].Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -228,7 +234,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/meetings/" + meeting.StringId; + string route = $"/meetings/{meeting.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -245,7 +251,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + meeting.StringId + @""", ""attributes"": { ""title"": """ + meeting.Title + @""", - ""startTime"": """ + meeting.StartTime.ToString("O") + @""", + ""startTime"": """ + meeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + meeting.Duration + @""", ""location"": { ""lat"": " + meeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -271,9 +277,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => public async Task Cannot_get_unknown_primary_resource_by_ID() { // Arrange - var unknownId = Guid.NewGuid(); + string meetingId = Unknown.StringId.For(); - string route = "/meetings/" + unknownId; + string route = $"/meetings/{meetingId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteGetAsync(route); @@ -281,10 +287,7 @@ public async Task Cannot_get_unknown_primary_resource_by_ID() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); - var jObject = JsonConvert.DeserializeObject(responseDocument); - jObject.Should().NotBeNull(); - - string errorId = jObject!["errors"].Should().NotBeNull().And.Subject.Select(element => (string)element["id"]).Single(); + string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); responseDocument.Should().BeJson(@"{ ""errors"": [ @@ -292,7 +295,7 @@ public async Task Cannot_get_unknown_primary_resource_by_ID() ""id"": """ + errorId + @""", ""status"": ""404"", ""title"": ""The requested resource does not exist."", - ""detail"": ""Resource of type 'meetings' with ID '" + unknownId + @"' does not exist."" + ""detail"": ""Resource of type 'meetings' with ID '" + meetingId + @"' does not exist."" } ] }"); @@ -328,7 +331,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""id"": """ + attendee.Meeting.StringId + @""", ""attributes"": { ""title"": """ + attendee.Meeting.Title + @""", - ""startTime"": """ + attendee.Meeting.StartTime.ToString("O") + @""", + ""startTime"": """ + attendee.Meeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + attendee.Meeting.Duration + @""", ""location"": { ""lat"": " + attendee.Meeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -575,7 +578,7 @@ public async Task Can_create_resource_with_side_effects() ""id"": """ + newMeeting.StringId + @""", ""attributes"": { ""title"": """ + newMeeting.Title + @""", - ""startTime"": """ + newMeeting.StartTime.ToString("O") + @""", + ""startTime"": """ + newMeeting.StartTime.ToString(JsonDateTimeOffsetFormatSpecifier) + @""", ""duration"": """ + newMeeting.Duration + @""", ""location"": { ""lat"": " + newMeeting.Location.Latitude.ToString(CultureInfo.InvariantCulture) + @", @@ -622,7 +625,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/meetingAttendees/" + existingAttendee.StringId; + string route = $"/meetingAttendees/{existingAttendee.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -655,6 +658,52 @@ await _testContext.RunOnDatabaseAsync(async dbContext => }"); } + [Fact] + public async Task Can_update_resource_with_relationship_for_type_at_end() + { + // Arrange + MeetingAttendee existingAttendee = _fakers.MeetingAttendee.Generate(); + existingAttendee.Meeting = _fakers.Meeting.Generate(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.Attendees.Add(existingAttendee); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + id = existingAttendee.StringId, + attributes = new + { + displayName = existingAttendee.DisplayName + }, + relationships = new + { + meeting = new + { + data = new + { + id = existingAttendee.Meeting.StringId, + type = "meetings" + } + } + }, + type = "meetingAttendees" + } + }; + + string route = $"/meetingAttendees/{existingAttendee.StringId}"; + + // Act + (HttpResponseMessage httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + } + [Fact] public async Task Includes_version_on_resource_endpoint() { @@ -686,6 +735,40 @@ await _testContext.RunOnDatabaseAsync(async dbContext => ""self"": ""http://localhost/meetingAttendees/" + attendee.StringId + @"/meeting"" }, ""data"": null +}"); + } + + [Fact] + public async Task Includes_version_on_error_in_resource_endpoint() + { + // Arrange + var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService(); + options.IncludeJsonApiVersion = true; + + string attendeeId = Unknown.StringId.For(); + + string route = $"/meetingAttendees/{attendeeId}"; + + // Act + (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + string errorId = JsonApiStringConverter.ExtractErrorId(responseDocument); + + responseDocument.Should().BeJson(@"{ + ""jsonapi"": { + ""version"": ""1.1"" + }, + ""errors"": [ + { + ""id"": """ + errorId + @""", + ""status"": ""404"", + ""title"": ""The requested resource does not exist."", + ""detail"": ""Resource of type 'meetingAttendees' with ID '" + attendeeId + @"' does not exist."" + } + ] }"); } } diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs index 18a6ac65cb..702aa7cee3 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion/SoftDeletionTests.cs @@ -65,8 +65,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(departments[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(departments[1].StringId); } [Fact] @@ -97,8 +97,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(departments[0].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(departments[0].StringId); } [Fact] @@ -128,9 +128,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Type.Should().Be("companies"); - responseDocument.ManyData[0].Id.Should().Be(companies[1].StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Type.Should().Be("companies"); + responseDocument.Data.ManyValue[0].Id.Should().Be(companies[1].StringId); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Type.Should().Be("departments"); @@ -150,17 +150,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/departments/" + department.StringId; + string route = $"/departments/{department.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); @@ -183,14 +183,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{company.StringId}/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); @@ -218,8 +218,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); } [Fact] @@ -239,14 +239,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{department.StringId}/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); @@ -274,7 +274,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -294,14 +294,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{company.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{company.StringId}' does not exist."); @@ -329,8 +329,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be(company.Departments.ElementAt(1).StringId); } [Fact] @@ -350,14 +350,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{department.StringId}/relationships/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteGetAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{department.StringId}' does not exist."); @@ -385,7 +385,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.Data.Should().BeNull(); + responseDocument.Data.Value.Should().BeNull(); } [Fact] @@ -432,14 +432,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/companies"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -488,14 +488,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => const string route = "/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'companies' with ID '{existingCompany.StringId}' in relationship 'company' does not exist."); @@ -529,17 +529,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/companies/" + existingCompany.StringId; + string route = $"/companies/{existingCompany.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -583,17 +583,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/companies/" + existingCompany.StringId; + string route = $"/companies/{existingCompany.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -636,17 +636,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = "/departments/" + existingDepartment.StringId; + string route = $"/departments/{existingDepartment.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'companies' with ID '{existingCompany.StringId}' in relationship 'company' does not exist."); @@ -674,14 +674,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -717,14 +717,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -753,14 +753,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{existingDepartment.StringId}/relationships/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{existingDepartment.StringId}' does not exist."); @@ -793,14 +793,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/departments/{existingDepartment.StringId}/relationships/company"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); error.Detail.Should().Be($"Related resource of type 'companies' with ID '{existingCompany.StringId}' in relationship 'company' does not exist."); @@ -836,14 +836,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -879,14 +879,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -923,14 +923,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'companies' with ID '{existingCompany.StringId}' does not exist."); @@ -965,14 +965,14 @@ await _testContext.RunOnDatabaseAsync(async dbContext => string route = $"/companies/{existingCompany.StringId}/relationships/departments"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route, requestBody); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("A related resource does not exist."); @@ -992,7 +992,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/companies/" + existingCompany.StringId; + string route = $"/companies/{existingCompany.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecuteDeleteAsync(route); @@ -1024,17 +1024,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/departments/" + existingDepartment.StringId; + string route = $"/departments/{existingDepartment.StringId}"; // Act - (HttpResponseMessage httpResponse, ErrorDocument responseDocument) = await _testContext.ExecuteDeleteAsync(route); + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteDeleteAsync(route); // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); responseDocument.Errors.Should().HaveCount(1); - Error error = responseDocument.Errors[0]; + ErrorObject error = responseDocument.Errors[0]; error.StatusCode.Should().Be(HttpStatusCode.NotFound); error.Title.Should().Be("The requested resource does not exist."); error.Detail.Should().Be($"Resource of type 'departments' with ID '{existingDepartment.StringId}' does not exist."); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs index 37f982762c..5fa4761150 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/EmptyGuidAsKeyTests.cs @@ -54,9 +54,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be("00000000-0000-0000-0000-000000000000"); - responseDocument.ManyData[0].Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be("00000000-0000-0000-0000-000000000000"); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); } [Fact] @@ -83,9 +83,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("00000000-0000-0000-0000-000000000000"); - responseDocument.SingleData.Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("00000000-0000-0000-0000-000000000000"); + responseDocument.Data.SingleValue.Links.Self.Should().Be("/maps/00000000-0000-0000-0000-000000000000"); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be("0"); @@ -157,7 +157,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "maps", - id = Guid.Empty, + id = "00000000-0000-0000-0000-000000000000", attributes = new { name = newName @@ -244,7 +244,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "maps", - id = Guid.Empty + id = "00000000-0000-0000-0000-000000000000" } }; @@ -289,7 +289,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "maps", - id = Guid.Empty + id = "00000000-0000-0000-0000-000000000000" } }; diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs index a1e54cfd59..04133f348b 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ZeroKeys/ZeroAsKeyTests.cs @@ -54,9 +54,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Id.Should().Be("0"); - responseDocument.ManyData[0].Links.Self.Should().Be("/games/0"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Id.Should().Be("0"); + responseDocument.Data.ManyValue[0].Links.Self.Should().Be("/games/0"); } [Fact] @@ -82,9 +82,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("0"); - responseDocument.SingleData.Links.Self.Should().Be("/games/0"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("0"); + responseDocument.Data.SingleValue.Links.Self.Should().Be("/games/0"); responseDocument.Included.Should().HaveCount(1); responseDocument.Included[0].Id.Should().Be(game.ActivePlayers.ElementAt(0).StringId); @@ -124,8 +124,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => httpResponse.Headers.Location.Should().Be("/games/0"); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("0"); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("0"); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -173,9 +173,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be("0"); - responseDocument.SingleData.Attributes["title"].Should().Be(newTitle); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be("0"); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(newTitle); await _testContext.RunOnDatabaseAsync(async dbContext => { @@ -246,7 +246,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => data = new { type = "games", - id = 0 + id = "0" } }; @@ -376,7 +376,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; @@ -425,7 +425,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; @@ -474,7 +474,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; @@ -521,7 +521,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => new { type = "games", - id = 0 + id = "0" } } }; diff --git a/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj b/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj index c313e73850..efb9e23173 100644 --- a/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj +++ b/test/JsonApiDotNetCoreTests/JsonApiDotNetCoreTests.csproj @@ -15,6 +15,7 @@ + diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs deleted file mode 100644 index f775273cbb..0000000000 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/DefaultsParseTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Net; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.QueryStrings.Internal; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreTests.UnitTests.QueryStringParameters -{ - public sealed class DefaultsParseTests - { - private readonly IDefaultsQueryStringParameterReader _reader; - - public DefaultsParseTests() - { - _reader = new DefaultsQueryStringParameterReader(new JsonApiOptions()); - } - - [Theory] - [InlineData("defaults", true)] - [InlineData("default", false)] - [InlineData("defaultsettings", false)] - public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanParse) - { - // Act - bool canParse = _reader.CanRead(parameterName); - - // Assert - canParse.Should().Be(expectCanParse); - } - - [Theory] - [InlineData(JsonApiQueryStringParameters.Defaults, false, false)] - [InlineData(JsonApiQueryStringParameters.Defaults, true, false)] - [InlineData(JsonApiQueryStringParameters.All, false, false)] - [InlineData(JsonApiQueryStringParameters.All, true, false)] - [InlineData(JsonApiQueryStringParameters.None, false, false)] - [InlineData(JsonApiQueryStringParameters.None, true, true)] - [InlineData(JsonApiQueryStringParameters.Filter, false, false)] - [InlineData(JsonApiQueryStringParameters.Filter, true, true)] - public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) - { - // Arrange - var options = new JsonApiOptions - { - AllowQueryStringOverrideForSerializerDefaultValueHandling = allowOverride - }; - - var reader = new DefaultsQueryStringParameterReader(options); - - // Act - bool isEnabled = reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); - - // Assert - isEnabled.Should().Be(allowOverride && expectIsEnabled); - } - - [Theory] - [InlineData("defaults", "", "The value '' must be 'true' or 'false'.")] - [InlineData("defaults", " ", "The value ' ' must be 'true' or 'false'.")] - [InlineData("defaults", "null", "The value 'null' must be 'true' or 'false'.")] - [InlineData("defaults", "0", "The value '0' must be 'true' or 'false'.")] - [InlineData("defaults", "1", "The value '1' must be 'true' or 'false'.")] - [InlineData("defaults", "-1", "The value '-1' must be 'true' or 'false'.")] - public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) - { - // Act - Action action = () => _reader.Read(parameterName, parameterValue); - - // Assert - InvalidQueryStringParameterException exception = action.Should().ThrowExactly().And; - - exception.QueryParameterName.Should().Be(parameterName); - exception.Errors.Should().HaveCount(1); - exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - exception.Errors[0].Title.Should().Be("The specified defaults is invalid."); - exception.Errors[0].Detail.Should().Be(errorMessage); - exception.Errors[0].Source.Parameter.Should().Be(parameterName); - } - - [Theory] - [InlineData("defaults", "true", DefaultValueHandling.Include)] - [InlineData("defaults", "True", DefaultValueHandling.Include)] - [InlineData("defaults", "false", DefaultValueHandling.Ignore)] - [InlineData("defaults", "False", DefaultValueHandling.Ignore)] - public void Reader_Read_Succeeds(string parameterName, string parameterValue, DefaultValueHandling expectedValue) - { - // Act - _reader.Read(parameterName, parameterValue); - - DefaultValueHandling handling = _reader.SerializerDefaultValueHandling; - - // Assert - handling.Should().Be(expectedValue); - } - - [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 Reader_Outcome(string queryStringParameterValue, DefaultValueHandling optionsDefaultValue, bool optionsAllowOverride, - DefaultValueHandling expected) - { - // Arrange - var options = new JsonApiOptions - { - SerializerSettings = - { - DefaultValueHandling = optionsDefaultValue - }, - AllowQueryStringOverrideForSerializerDefaultValueHandling = optionsAllowOverride - }; - - var reader = new DefaultsQueryStringParameterReader(options); - - // Act - if (reader.IsEnabled(DisableQueryStringAttribute.Empty)) - { - reader.Read("defaults", queryStringParameterValue); - } - - // Assert - reader.SerializerDefaultValueHandling.Should().Be(expected); - } - } -} diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs deleted file mode 100644 index c5e83833b9..0000000000 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/NullsParseTests.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Net; -using FluentAssertions; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers.Annotations; -using JsonApiDotNetCore.Errors; -using JsonApiDotNetCore.QueryStrings; -using JsonApiDotNetCore.QueryStrings.Internal; -using Newtonsoft.Json; -using Xunit; - -namespace JsonApiDotNetCoreTests.UnitTests.QueryStringParameters -{ - public sealed class NullsParseTests - { - private readonly INullsQueryStringParameterReader _reader; - - public NullsParseTests() - { - _reader = new NullsQueryStringParameterReader(new JsonApiOptions()); - } - - [Theory] - [InlineData("nulls", true)] - [InlineData("null", false)] - [InlineData("nullsettings", false)] - public void Reader_Supports_Parameter_Name(string parameterName, bool expectCanParse) - { - // Act - bool canParse = _reader.CanRead(parameterName); - - // Assert - canParse.Should().Be(expectCanParse); - } - - [Theory] - [InlineData(JsonApiQueryStringParameters.Nulls, false, false)] - [InlineData(JsonApiQueryStringParameters.Nulls, true, false)] - [InlineData(JsonApiQueryStringParameters.All, false, false)] - [InlineData(JsonApiQueryStringParameters.All, true, false)] - [InlineData(JsonApiQueryStringParameters.None, false, false)] - [InlineData(JsonApiQueryStringParameters.None, true, true)] - [InlineData(JsonApiQueryStringParameters.Filter, false, false)] - [InlineData(JsonApiQueryStringParameters.Filter, true, true)] - public void Reader_Is_Enabled(JsonApiQueryStringParameters parametersDisabled, bool allowOverride, bool expectIsEnabled) - { - // Arrange - var options = new JsonApiOptions - { - AllowQueryStringOverrideForSerializerNullValueHandling = allowOverride - }; - - var reader = new NullsQueryStringParameterReader(options); - - // Act - bool isEnabled = reader.IsEnabled(new DisableQueryStringAttribute(parametersDisabled)); - - // Assert - isEnabled.Should().Be(allowOverride && expectIsEnabled); - } - - [Theory] - [InlineData("nulls", "", "The value '' must be 'true' or 'false'.")] - [InlineData("nulls", " ", "The value ' ' must be 'true' or 'false'.")] - [InlineData("nulls", "null", "The value 'null' must be 'true' or 'false'.")] - [InlineData("nulls", "0", "The value '0' must be 'true' or 'false'.")] - [InlineData("nulls", "1", "The value '1' must be 'true' or 'false'.")] - [InlineData("nulls", "-1", "The value '-1' must be 'true' or 'false'.")] - public void Reader_Read_Fails(string parameterName, string parameterValue, string errorMessage) - { - // Act - Action action = () => _reader.Read(parameterName, parameterValue); - - // Assert - InvalidQueryStringParameterException exception = action.Should().ThrowExactly().And; - - exception.QueryParameterName.Should().Be(parameterName); - exception.Errors.Should().HaveCount(1); - exception.Errors[0].StatusCode.Should().Be(HttpStatusCode.BadRequest); - exception.Errors[0].Title.Should().Be("The specified nulls is invalid."); - exception.Errors[0].Detail.Should().Be(errorMessage); - exception.Errors[0].Source.Parameter.Should().Be(parameterName); - } - - [Theory] - [InlineData("nulls", "true", NullValueHandling.Include)] - [InlineData("nulls", "True", NullValueHandling.Include)] - [InlineData("nulls", "false", NullValueHandling.Ignore)] - [InlineData("nulls", "False", NullValueHandling.Ignore)] - public void Reader_Read_Succeeds(string parameterName, string parameterValue, NullValueHandling expectedValue) - { - // Act - _reader.Read(parameterName, parameterValue); - - NullValueHandling handling = _reader.SerializerNullValueHandling; - - // Assert - handling.Should().Be(expectedValue); - } - - [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 Reader_Outcome(string queryStringParameterValue, NullValueHandling optionsNullValue, bool optionsAllowOverride, NullValueHandling expected) - { - // Arrange - var options = new JsonApiOptions - { - SerializerSettings = - { - NullValueHandling = optionsNullValue - }, - AllowQueryStringOverrideForSerializerNullValueHandling = optionsAllowOverride - }; - - var reader = new NullsQueryStringParameterReader(options); - - // Act - if (reader.IsEnabled(DisableQueryStringAttribute.Empty)) - { - reader.Read("nulls", queryStringParameterValue); - } - - // Assert - reader.SerializerNullValueHandling.Should().Be(expected); - } - } -} diff --git a/test/MultiDbContextTests/ResourceTests.cs b/test/MultiDbContextTests/ResourceTests.cs index ece21a33a4..27b2049e07 100644 --- a/test/MultiDbContextTests/ResourceTests.cs +++ b/test/MultiDbContextTests/ResourceTests.cs @@ -1,9 +1,12 @@ using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; using MultiDbContextExample; using TestBuildingBlocks; using Xunit; @@ -14,6 +17,15 @@ public sealed class ResourceTests : IntegrationTest, IClassFixture _factory; + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = _factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + public ResourceTests(WebApplicationFactory factory) { _factory = factory; @@ -31,8 +43,8 @@ public async Task Can_get_ResourceAs() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["nameA"].Should().Be("SampleA"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["nameA"].Should().Be("SampleA"); } [Fact] @@ -47,8 +59,8 @@ public async Task Can_get_ResourceBs() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().HaveCount(1); - responseDocument.ManyData[0].Attributes["nameB"].Should().Be("SampleB"); + responseDocument.Data.ManyValue.Should().HaveCount(1); + responseDocument.Data.ManyValue[0].Attributes["nameB"].Should().Be("SampleB"); } protected override HttpClient CreateClient() diff --git a/test/NoEntityFrameworkTests/WorkItemTests.cs b/test/NoEntityFrameworkTests/WorkItemTests.cs index 2bf2f64c2a..e1b6dbafe6 100644 --- a/test/NoEntityFrameworkTests/WorkItemTests.cs +++ b/test/NoEntityFrameworkTests/WorkItemTests.cs @@ -1,8 +1,10 @@ using System; using System.Net; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using FluentAssertions; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; @@ -18,6 +20,15 @@ public sealed class WorkItemTests : IntegrationTest, IClassFixture _factory; + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = _factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + public WorkItemTests(WebApplicationFactory factory) { _factory = factory; @@ -41,7 +52,7 @@ await RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.ManyData.Should().NotBeEmpty(); + responseDocument.Data.ManyValue.Should().NotBeEmpty(); } [Fact] @@ -56,7 +67,7 @@ await RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/v1/workItems/" + workItem.StringId; + string route = $"/api/v1/workItems/{workItem.StringId}"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await ExecuteGetAsync(route); @@ -64,8 +75,8 @@ await RunOnDatabaseAsync(async dbContext => // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Id.Should().Be(workItem.StringId); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Id.Should().Be(workItem.StringId); } [Fact] @@ -103,11 +114,11 @@ public async Task Can_create_WorkItem() // Assert httpResponse.Should().HaveStatusCode(HttpStatusCode.Created); - responseDocument.SingleData.Should().NotBeNull(); - responseDocument.SingleData.Attributes["isBlocked"].Should().Be(newWorkItem.IsBlocked); - responseDocument.SingleData.Attributes["title"].Should().Be(newWorkItem.Title); - responseDocument.SingleData.Attributes["durationInHours"].Should().Be(newWorkItem.DurationInHours); - responseDocument.SingleData.Attributes["projectId"].Should().Be(newWorkItem.ProjectId.ToString()); + responseDocument.Data.SingleValue.Should().NotBeNull(); + responseDocument.Data.SingleValue.Attributes["isBlocked"].Should().Be(newWorkItem.IsBlocked); + responseDocument.Data.SingleValue.Attributes["title"].Should().Be(newWorkItem.Title); + responseDocument.Data.SingleValue.Attributes["durationInHours"].Should().Be(newWorkItem.DurationInHours); + responseDocument.Data.SingleValue.Attributes["projectId"].Should().Be(newWorkItem.ProjectId); } [Fact] @@ -122,7 +133,7 @@ await RunOnDatabaseAsync(async dbContext => await dbContext.SaveChangesAsync(); }); - string route = "/api/v1/workItems/" + workItem.StringId; + string route = $"/api/v1/workItems/{workItem.StringId}"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await ExecuteDeleteAsync(route); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index dfa5891dca..14e3dfa130 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -187,7 +187,7 @@ public async Task Getting_resource_translates_response() document.Data.Attributes.ServicesOnBoard.Should().BeNull(); document.Data.Attributes.FinalDestination.Should().BeNull(); document.Data.Attributes.StopOverDestination.Should().BeNull(); - document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); + document.Data.Attributes.OperatedBy.Should().Be(default); } [Fact] diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs b/test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs new file mode 100644 index 0000000000..bd98c17e22 --- /dev/null +++ b/test/OpenApiTests/LegacyOpenApiIntegration/JsonKebabCaseNamingPolicy.cs @@ -0,0 +1,83 @@ +using System; +using System.Text; +using System.Text.Json; + +namespace OpenApiTests.LegacyOpenApiIntegration +{ + // Based on https://github.com/J0rgeSerran0/JsonNamingPolicy + internal sealed class JsonKebabCaseNamingPolicy : JsonNamingPolicy + { + private const char Separator = '-'; + + public static readonly JsonKebabCaseNamingPolicy Instance = new(); + + public override string ConvertName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return string.Empty; + } + + ReadOnlySpan spanName = name.Trim(); + + var stringBuilder = new StringBuilder(); + bool addCharacter = true; + + bool isNextLower = false; + bool isNextUpper = false; + bool isNextSpace = false; + + for (int position = 0; position < spanName.Length; position++) + { + if (position != 0) + { + bool isCurrentSpace = spanName[position] == 32; + bool isPreviousSpace = spanName[position - 1] == 32; + bool isPreviousSeparator = spanName[position - 1] == 95; + + if (position + 1 != spanName.Length) + { + isNextLower = spanName[position + 1] > 96 && spanName[position + 1] < 123; + isNextUpper = spanName[position + 1] > 64 && spanName[position + 1] < 91; + isNextSpace = spanName[position + 1] == 32; + } + + if (isCurrentSpace && (isPreviousSpace || isPreviousSeparator || isNextUpper || isNextSpace)) + { + addCharacter = false; + } + else + { + bool isCurrentUpper = spanName[position] > 64 && spanName[position] < 91; + bool isPreviousLower = spanName[position - 1] > 96 && spanName[position - 1] < 123; + bool isPreviousNumber = spanName[position - 1] > 47 && spanName[position - 1] < 58; + + if (isCurrentUpper && (isPreviousLower || isPreviousNumber || isNextLower || isNextSpace)) + { + stringBuilder.Append(Separator); + } + else + { + if (isCurrentSpace) + { + stringBuilder.Append(Separator); + addCharacter = false; + } + } + } + } + + if (addCharacter) + { + stringBuilder.Append(spanName[position]); + } + else + { + addCharacter = true; + } + } + + return stringBuilder.ToString().ToLower(); + } + } +} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index 02dbf96e25..916a40fa4d 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -1,8 +1,8 @@ +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json.Serialization; namespace OpenApiTests.LegacyOpenApiIntegration { @@ -16,11 +16,9 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.Namespace = "api/v1"; options.DefaultAttrCapabilities = AttrCapabilities.AllowView; - - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new KebabCaseNamingStrategy() - }; + options.SerializerOptions.PropertyNamingPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.DictionaryKeyPolicy = JsonKebabCaseNamingPolicy.Instance; + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 3ed426bf24..d8c9bbda06 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -1870,6 +1870,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/airplane-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -1879,12 +1885,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/airplane-data-in-response" - } } }, "additionalProperties": false @@ -1990,6 +1990,9 @@ ], "type": "object", "properties": { + "data": { + "$ref": "#/components/schemas/airplane-data-in-response" + }, "meta": { "type": "object", "additionalProperties": {} @@ -1999,9 +2002,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "$ref": "#/components/schemas/airplane-data-in-response" } }, "additionalProperties": false @@ -2119,6 +2119,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2128,12 +2134,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" - } } }, "additionalProperties": false @@ -2231,6 +2231,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2240,12 +2246,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-attendant-identifier" - } } }, "additionalProperties": false @@ -2257,16 +2257,6 @@ ], "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" - }, "data": { "oneOf": [ { @@ -2276,6 +2266,16 @@ "$ref": "#/components/schemas/null-value" } ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" } }, "additionalProperties": false @@ -2311,6 +2311,9 @@ ], "type": "object", "properties": { + "data": { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + }, "meta": { "type": "object", "additionalProperties": {} @@ -2320,9 +2323,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" } }, "additionalProperties": false @@ -2370,16 +2370,6 @@ ], "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-document" - }, "data": { "oneOf": [ { @@ -2389,6 +2379,16 @@ "$ref": "#/components/schemas/null-value" } ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" } }, "additionalProperties": false @@ -2459,6 +2459,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2468,12 +2474,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-data-in-response" - } } }, "additionalProperties": false @@ -2568,6 +2568,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2577,12 +2583,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-identifier" - } } }, "additionalProperties": false @@ -2618,6 +2618,9 @@ ], "type": "object", "properties": { + "data": { + "$ref": "#/components/schemas/flight-data-in-response" + }, "meta": { "type": "object", "additionalProperties": {} @@ -2627,9 +2630,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "$ref": "#/components/schemas/flight-data-in-response" } }, "additionalProperties": false @@ -2875,6 +2875,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-data-in-response" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2884,12 +2890,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/passenger-data-in-response" - } } }, "additionalProperties": false @@ -2944,6 +2944,12 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } + }, "meta": { "type": "object", "additionalProperties": {} @@ -2953,12 +2959,6 @@ }, "links": { "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/passenger-identifier" - } } }, "additionalProperties": false @@ -2990,18 +2990,18 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + }, "links": { "$ref": "#/components/schemas/links-in-relationship-object" }, "meta": { "type": "object", "additionalProperties": {} - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-attendant-identifier" - } } }, "additionalProperties": false @@ -3027,18 +3027,18 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } + }, "links": { "$ref": "#/components/schemas/links-in-relationship-object" }, "meta": { "type": "object", "additionalProperties": {} - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/flight-identifier" - } } }, "additionalProperties": false @@ -3064,18 +3064,18 @@ ], "type": "object", "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } + }, "links": { "$ref": "#/components/schemas/links-in-relationship-object" }, "meta": { "type": "object", "additionalProperties": {} - }, - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/passenger-identifier" - } } }, "additionalProperties": false @@ -3105,13 +3105,6 @@ ], "type": "object", "properties": { - "links": { - "$ref": "#/components/schemas/links-in-relationship-object" - }, - "meta": { - "type": "object", - "additionalProperties": {} - }, "data": { "oneOf": [ { @@ -3121,6 +3114,13 @@ "$ref": "#/components/schemas/null-value" } ] + }, + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} } }, "additionalProperties": false diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 04355533a7..712134cb82 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -4,7 +4,6 @@ - diff --git a/test/TestBuildingBlocks/DbContextExtensions.cs b/test/TestBuildingBlocks/DbContextExtensions.cs index 329d6f1633..389038f7d1 100644 --- a/test/TestBuildingBlocks/DbContextExtensions.cs +++ b/test/TestBuildingBlocks/DbContextExtensions.cs @@ -51,11 +51,11 @@ private static async Task ClearTablesAsync(this DbContext dbContext, params Type // In that case, we recursively delete all related data, which is slow. try { - await dbContext.Database.ExecuteSqlRawAsync("delete from \"" + tableName + "\""); + await dbContext.Database.ExecuteSqlRawAsync($"delete from \"{tableName}\""); } catch (PostgresException) { - await dbContext.Database.ExecuteSqlRawAsync("truncate table \"" + tableName + "\" cascade"); + await dbContext.Database.ExecuteSqlRawAsync($"truncate table \"{tableName}\" cascade"); } } } diff --git a/test/TestBuildingBlocks/FakerContainer.cs b/test/TestBuildingBlocks/FakerContainer.cs index a324c9dacb..2dbfb441b8 100644 --- a/test/TestBuildingBlocks/FakerContainer.cs +++ b/test/TestBuildingBlocks/FakerContainer.cs @@ -13,7 +13,7 @@ protected static int GetFakerSeed() // The goal here is to have stable data over multiple test runs, but at the same time different data per test case. MethodBase testMethod = GetTestMethod(); - string testName = testMethod.DeclaringType?.FullName + "." + testMethod.Name; + string testName = $"{testMethod.DeclaringType?.FullName}.{testMethod.Name}"; return GetDeterministicHashCode(testName); } diff --git a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs index 681166358a..873cec6d3f 100644 --- a/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs +++ b/test/TestBuildingBlocks/HttpResponseMessageExtensions.cs @@ -1,11 +1,8 @@ using System.Net; using System.Net.Http; -using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Primitives; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace TestBuildingBlocks { @@ -22,8 +19,8 @@ public sealed class HttpResponseMessageAssertions : ReferenceTypeAssertions "response"; public HttpResponseMessageAssertions(HttpResponseMessage instance) + : base(instance) { - Subject = instance; } // ReSharper disable once UnusedMethodReturnValue.Global @@ -32,33 +29,12 @@ public AndConstraint HaveStatusCode(HttpStatusCod { if (Subject.StatusCode != statusCode) { - string responseText = GetFormattedContentAsync(Subject).Result; - Subject.StatusCode.Should().Be(statusCode, "response body returned was:\n" + responseText); + string responseText = Subject.Content.ReadAsStringAsync().Result; + Subject.StatusCode.Should().Be(statusCode, $"response body returned was:\n{responseText}"); } return new AndConstraint(this); } - - private static async Task GetFormattedContentAsync(HttpResponseMessage responseMessage) - { - string text = await responseMessage.Content.ReadAsStringAsync(); - - try - { - if (text.Length > 0) - { - return JsonConvert.DeserializeObject(text)?.ToString(); - } - } -#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - catch -#pragma warning restore AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - { - // ignored - } - - return text; - } } } } diff --git a/test/TestBuildingBlocks/IntegrationTest.cs b/test/TestBuildingBlocks/IntegrationTest.cs index 6de6ca28a5..59e45b0b72 100644 --- a/test/TestBuildingBlocks/IntegrationTest.cs +++ b/test/TestBuildingBlocks/IntegrationTest.cs @@ -2,9 +2,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using JsonApiDotNetCore.Middleware; -using Newtonsoft.Json; namespace TestBuildingBlocks { @@ -13,7 +13,7 @@ namespace TestBuildingBlocks /// public abstract class IntegrationTest { - private static readonly IntegrationTestConfiguration IntegrationTestConfiguration = new(); + protected abstract JsonSerializerOptions SerializerOptions { get; } public async Task<(HttpResponseMessage httpResponse, TResponseDocument responseDocument)> ExecuteHeadAsync(string requestUrl, Action setRequestHeaders = null) @@ -82,7 +82,8 @@ public abstract class IntegrationTest private string SerializeRequest(object requestBody) { - return requestBody == null ? null : requestBody is string stringRequestBody ? stringRequestBody : JsonConvert.SerializeObject(requestBody); + return requestBody == null ? null : + requestBody is string stringRequestBody ? stringRequestBody : JsonSerializer.Serialize(requestBody, SerializerOptions); } protected abstract HttpClient CreateClient(); @@ -96,7 +97,7 @@ private TResponseDocument DeserializeResponse(string response try { - return JsonConvert.DeserializeObject(responseText, IntegrationTestConfiguration.DeserializationSettings); + return JsonSerializer.Deserialize(responseText, SerializerOptions); } catch (JsonException exception) { diff --git a/test/TestBuildingBlocks/IntegrationTestConfiguration.cs b/test/TestBuildingBlocks/IntegrationTestConfiguration.cs deleted file mode 100644 index 30c1c0328e..0000000000 --- a/test/TestBuildingBlocks/IntegrationTestConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace TestBuildingBlocks -{ - internal sealed class IntegrationTestConfiguration - { - // Because our tests often deserialize incoming responses into weakly-typed string-to-object dictionaries (as part of ResourceObject), - // Newtonsoft.JSON is unable to infer the target type in such cases. So we steer a bit using explicit configuration. - public readonly JsonSerializerSettings DeserializationSettings = new() - { - // Choosing between DateTime and DateTimeOffset is impossible: it depends on how the resource properties are declared. - // So instead we leave them as strings and let the test itself deal with the conversion. - DateParseHandling = DateParseHandling.None, - - // Here we must choose between double (default) and decimal. Favored decimal because it has higher precision (but lower range). - FloatParseHandling = FloatParseHandling.Decimal - }; - } -} diff --git a/test/TestBuildingBlocks/IntegrationTestContext.cs b/test/TestBuildingBlocks/IntegrationTestContext.cs index b690374c93..68550f4c26 100644 --- a/test/TestBuildingBlocks/IntegrationTestContext.cs +++ b/test/TestBuildingBlocks/IntegrationTestContext.cs @@ -1,7 +1,9 @@ using System; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Testing; @@ -35,6 +37,15 @@ public class IntegrationTestContext : IntegrationTest, IDi private Action _beforeServicesConfiguration; private Action _afterServicesConfiguration; + protected override JsonSerializerOptions SerializerOptions + { + get + { + var options = Factory.Services.GetRequiredService(); + return options.SerializerOptions; + } + } + public WebApplicationFactory Factory => _lazyFactory.Value; public IntegrationTestContext() diff --git a/test/TestBuildingBlocks/JsonApiStringConverter.cs b/test/TestBuildingBlocks/JsonApiStringConverter.cs new file mode 100644 index 0000000000..40b334cac1 --- /dev/null +++ b/test/TestBuildingBlocks/JsonApiStringConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Linq; +using System.Text.Json; + +#pragma warning disable AV1008 // Class should not be static +#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException + +namespace TestBuildingBlocks +{ + public static class JsonApiStringConverter + { + public static string ExtractErrorId(string responseBody) + { + try + { + using JsonDocument document = JsonDocument.Parse(responseBody); + return document.RootElement.GetProperty("errors").EnumerateArray().Single().GetProperty("id").GetString(); + } + catch (Exception exception) + { + throw new Exception($"Failed to extract Error ID from response body '{responseBody}'.", exception); + } + } + } +} diff --git a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs index 3efdd034a3..90edc1a544 100644 --- a/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs +++ b/test/TestBuildingBlocks/ObjectAssertionsExtensions.cs @@ -1,10 +1,12 @@ using System; +using System.IO; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; using FluentAssertions; using FluentAssertions.Numeric; using FluentAssertions.Primitives; using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace TestBuildingBlocks { @@ -12,33 +14,34 @@ namespace TestBuildingBlocks public static class ObjectAssertionsExtensions { private const decimal NumericPrecision = 0.00000000001M; + private static readonly TimeSpan TimePrecision = TimeSpan.FromMilliseconds(20); - private static readonly JsonSerializerSettings DeserializationSettings = new() + private static readonly JsonWriterOptions JsonWriterOptions = new() { - Formatting = Formatting.Indented + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; /// - /// Used to assert on a (nullable) or property, whose value is returned as in - /// JSON:API response body because of . + /// Same as , but with default precision. /// [CustomAssertion] - public static void BeCloseTo(this ObjectAssertions source, DateTimeOffset? expected, string because = "", params object[] becauseArgs) + public static AndConstraint BeCloseTo(this DateTimeAssertions parent, DateTime nearbyTime, string because = "", + params object[] becauseArgs) + where TAssertions : DateTimeAssertions { - if (expected == null) - { - source.Subject.Should().BeNull(because, becauseArgs); - } - else - { - if (!DateTimeOffset.TryParse((string)source.Subject, out DateTimeOffset value)) - { - source.Subject.Should().Be(expected, because, becauseArgs); - } + return parent.BeCloseTo(nearbyTime, TimePrecision, because, becauseArgs); + } - // We lose a little bit of precision (milliseconds) on roundtrip through PostgreSQL database. - value.Should().BeCloseTo(expected.Value, because: because, becauseArgs: becauseArgs); - } + /// + /// Same as , but with default precision. + /// + [CustomAssertion] + public static AndConstraint BeCloseTo(this DateTimeOffsetAssertions parent, DateTimeOffset nearbyTime, + string because = "", params object[] becauseArgs) + where TAssertions : DateTimeOffsetAssertions + { + return parent.BeCloseTo(nearbyTime, TimePrecision, because, becauseArgs); } /// @@ -69,13 +72,23 @@ public static AndConstraint> BeApproximately( [CustomAssertion] public static void BeJson(this StringAssertions source, string expected, string because = "", params object[] becauseArgs) { - var sourceToken = JsonConvert.DeserializeObject(source.Subject, DeserializationSettings); - var expectedToken = JsonConvert.DeserializeObject(expected, DeserializationSettings); + using JsonDocument sourceJson = JsonDocument.Parse(source.Subject); + using JsonDocument expectedJson = JsonDocument.Parse(expected); - string sourceText = sourceToken?.ToString(); - string expectedText = expectedToken?.ToString(); + string sourceText = ToJsonString(sourceJson); + string expectedText = ToJsonString(expectedJson); sourceText.Should().Be(expectedText, because, becauseArgs); } + + private static string ToJsonString(JsonDocument document) + { + using var stream = new MemoryStream(); + var writer = new Utf8JsonWriter(stream, JsonWriterOptions); + + document.WriteTo(writer); + writer.Flush(); + return Encoding.UTF8.GetString(stream.ToArray()); + } } } diff --git a/test/TestBuildingBlocks/TestableStartup.cs b/test/TestBuildingBlocks/TestableStartup.cs index 93705dfb2d..3f029e4eb6 100644 --- a/test/TestBuildingBlocks/TestableStartup.cs +++ b/test/TestBuildingBlocks/TestableStartup.cs @@ -4,8 +4,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace TestBuildingBlocks { @@ -20,8 +18,7 @@ public virtual void ConfigureServices(IServiceCollection services) protected virtual void SetJsonApiOptions(JsonApiOptions options) { options.IncludeExceptionStackTraceInErrors = true; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.Converters.Add(new StringEnumConverter()); + options.SerializerOptions.WriteIndented = true; } public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) diff --git a/test/TestBuildingBlocks/Unknown.cs b/test/TestBuildingBlocks/Unknown.cs new file mode 100644 index 0000000000..294490d96e --- /dev/null +++ b/test/TestBuildingBlocks/Unknown.cs @@ -0,0 +1,87 @@ +using System; +using JsonApiDotNetCore.Resources; + +// ReSharper disable MemberCanBeInternal +// ReSharper disable MemberCanBePrivate.Global +#pragma warning disable AV1008 // Class should not be static + +namespace TestBuildingBlocks +{ + public static class Unknown + { + public const string ResourceType = "doesNotExist1"; + public const string Relationship = "doesNotExist2"; + public const string LocalId = "doesNotExist3"; + + public static class TypedId + { + public const short Int16 = short.MaxValue; + public const short AltInt16 = Int16 - 1; + + public const int Int32 = int.MaxValue; + public const int AltInt32 = Int32 - 1; + + public const long Int64 = long.MaxValue; + public const long AltInt64 = Int64 - 1; + + public static readonly Guid Guid = Guid.Parse("FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"); + public static readonly Guid AltGuid = Guid.Parse("EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE"); + } + + public static class StringId + { + public static readonly string Int16 = TypedId.Int16.ToString(); + public static readonly string AltInt16 = TypedId.AltInt16.ToString(); + + public static readonly string Int32 = TypedId.Int32.ToString(); + public static readonly string AltInt32 = TypedId.AltInt32.ToString(); + + public static readonly string Int64 = TypedId.Int64.ToString(); + public static readonly string AltInt64 = TypedId.AltInt64.ToString(); + + public static readonly string Guid = TypedId.Guid.ToString(); + public static readonly string AltGuid = TypedId.AltGuid.ToString(); + + public static string For() + where TResource : IIdentifiable + { + return InnerFor(false); + } + + public static string AltFor() + where TResource : IIdentifiable + { + return InnerFor(true); + } + + private static string InnerFor(bool isAlt) + where TResource : IIdentifiable + { + Type type = typeof(TId); + + if (type == typeof(short)) + { + return isAlt ? AltInt16 : Int16; + } + + if (type == typeof(int)) + { + return isAlt ? AltInt32 : Int32; + } + + if (type == typeof(long)) + { + return isAlt ? AltInt64 : Int64; + } + + if (type == typeof(Guid)) + { + return isAlt ? AltGuid : Guid; + } + + throw new NotSupportedException( + $"Unsupported '{nameof(Identifiable.Id)}' property of type '{type}' on resource type '{typeof(TResource).Name}'."); + } + } + } +} diff --git a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs index 69e5efe56f..b6cd276c99 100644 --- a/test/UnitTests/Builders/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Builders/ResourceGraphBuilderTests.cs @@ -29,10 +29,10 @@ public void Can_Build_ResourceGraph_Using_Builder() // Assert var resourceGraph = container.GetRequiredService(); - ResourceContext dbResource = resourceGraph.GetResourceContext("dbResources"); - ResourceContext nonDbResource = resourceGraph.GetResourceContext("nonDbResources"); - Assert.Equal(typeof(DbResource), dbResource.ResourceType); - Assert.Equal(typeof(NonDbResource), nonDbResource.ResourceType); + ResourceContext dbResourceContext = resourceGraph.GetResourceContext("dbResources"); + ResourceContext nonDbResourceContext = resourceGraph.GetResourceContext("nonDbResources"); + Assert.Equal(typeof(DbResource), dbResourceContext.ResourceType); + Assert.Equal(typeof(NonDbResource), nonDbResourceContext.ResourceType); } [Fact] @@ -46,8 +46,8 @@ public void Resources_Without_Names_Specified_Will_Use_Configured_Formatter() IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("testResources", resource.PublicName); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Equal("testResources", resourceContext.PublicName); } [Fact] @@ -61,8 +61,8 @@ public void Attrs_Without_Names_Specified_Will_Use_Configured_Formatter() IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Contains(resource.Attributes, attribute => attribute.PublicName == "compoundAttribute"); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Contains(resourceContext.Attributes, attribute => attribute.PublicName == "compoundAttribute"); } [Fact] @@ -76,9 +76,9 @@ public void Relationships_Without_Names_Specified_Will_Use_Configured_Formatter( IResourceGraph resourceGraph = builder.Build(); // Assert - ResourceContext resource = resourceGraph.GetResourceContext(typeof(TestResource)); - Assert.Equal("relatedResource", resource.Relationships.Single(relationship => relationship is HasOneAttribute).PublicName); - Assert.Equal("relatedResources", resource.Relationships.Single(relationship => relationship is not HasOneAttribute).PublicName); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(TestResource)); + Assert.Equal("relatedResource", resourceContext.Relationships.Single(relationship => relationship is HasOneAttribute).PublicName); + Assert.Equal("relatedResources", resourceContext.Relationships.Single(relationship => relationship is not HasOneAttribute).PublicName); } private sealed class NonDbResource : Identifiable diff --git a/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs b/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs index 4b5ef50e47..90c0426d66 100644 --- a/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs +++ b/test/UnitTests/Controllers/BaseJsonApiControllerTests.cs @@ -26,7 +26,7 @@ public async Task GetAsync_Calls_Service() { // Arrange var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, serviceMock.Object); // Act await controller.GetAsync(CancellationToken.None); @@ -39,7 +39,7 @@ public async Task GetAsync_Calls_Service() public async Task GetAsync_Throws_405_If_No_Service() { // Arrange - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, null); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, null); // Act Func asyncAction = () => controller.GetAsync(CancellationToken.None); @@ -56,7 +56,7 @@ public async Task GetAsyncById_Calls_Service() // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, getById: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, getById: serviceMock.Object); // Act await controller.GetAsync(id, CancellationToken.None); @@ -70,7 +70,7 @@ public async Task GetAsyncById_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.GetAsync(id, CancellationToken.None); @@ -88,7 +88,7 @@ public async Task GetRelationshipsAsync_Calls_Service() const int id = 0; const string relationshipName = "articles"; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, getRelationship: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, getRelationship: serviceMock.Object); // Act await controller.GetRelationshipAsync(id, relationshipName, CancellationToken.None); @@ -102,7 +102,7 @@ public async Task GetRelationshipsAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.GetRelationshipAsync(id, "articles", CancellationToken.None); @@ -120,7 +120,7 @@ public async Task GetRelationshipAsync_Calls_Service() const int id = 0; const string relationshipName = "articles"; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, getSecondary: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, getSecondary: serviceMock.Object); // Act await controller.GetSecondaryAsync(id, relationshipName, CancellationToken.None); @@ -134,7 +134,7 @@ public async Task GetRelationshipAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.GetSecondaryAsync(id, "articles", CancellationToken.None); @@ -168,7 +168,7 @@ public async Task PatchAsync_Throws_405_If_No_Service() // Arrange const int id = 0; var resource = new Resource(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.PatchAsync(id, resource, CancellationToken.None); @@ -208,7 +208,7 @@ public async Task PatchRelationshipsAsync_Calls_Service() const int id = 0; const string relationshipName = "articles"; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, setRelationship: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, setRelationship: serviceMock.Object); // Act await controller.PatchRelationshipAsync(id, relationshipName, null, CancellationToken.None); @@ -222,7 +222,7 @@ public async Task PatchRelationshipsAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.PatchRelationshipAsync(id, "articles", null, CancellationToken.None); @@ -239,7 +239,7 @@ public async Task DeleteAsync_Calls_Service() // Arrange const int id = 0; var serviceMock = new Mock>(); - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance, delete: serviceMock.Object); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance, delete: serviceMock.Object); // Act await controller.DeleteAsync(id, CancellationToken.None); @@ -253,7 +253,7 @@ public async Task DeleteAsync_Throws_405_If_No_Service() { // Arrange const int id = 0; - var controller = new ResourceController(new Mock().Object, NullLoggerFactory.Instance); + var controller = new ResourceController(new JsonApiOptions(), NullLoggerFactory.Instance); // Act Func asyncAction = () => controller.DeleteAsync(id, CancellationToken.None); diff --git a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 2d24270655..542abd496a 100644 --- a/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/test/UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -28,15 +28,17 @@ public void RegisterResource_DeviatingDbContextPropertyName_RegistersCorrectly() var services = new ServiceCollection(); services.AddLogging(); services.AddDbContext(options => options.UseInMemoryDatabase("UnitTestDb")); - services.AddJsonApi(); - // Act // this is required because the DbContextResolver requires access to the current HttpContext // to get the request scoped DbContext instance services.AddScoped(); + + // Act + services.AddJsonApi(); + ServiceProvider provider = services.BuildServiceProvider(); - var graph = provider.GetRequiredService(); - ResourceContext resourceContext = graph.GetResourceContext(); + var resourceGraph = provider.GetRequiredService(); + ResourceContext resourceContext = resourceGraph.GetResourceContext(); // Assert Assert.Equal("people", resourceContext.PublicName); @@ -177,8 +179,9 @@ public void AddJsonApi_With_Context_Uses_Resource_Type_Name_If_NoOtherSpecified( // Assert ServiceProvider provider = services.BuildServiceProvider(); var resourceGraph = provider.GetRequiredService(); - ResourceContext resource = resourceGraph.GetResourceContext(typeof(IntResource)); - Assert.Equal("intResources", resource.PublicName); + + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(IntResource)); + Assert.Equal("intResources", resourceContext.PublicName); } private sealed class IntResource : Identifiable diff --git a/test/UnitTests/Internal/ErrorDocumentTests.cs b/test/UnitTests/Internal/ErrorDocumentTests.cs index 490ebcf91f..e2426659b2 100644 --- a/test/UnitTests/Internal/ErrorDocumentTests.cs +++ b/test/UnitTests/Internal/ErrorDocumentTests.cs @@ -18,7 +18,10 @@ public sealed class ErrorDocumentTests public void ErrorDocument_GetErrorStatusCode_IsCorrect(HttpStatusCode[] errorCodes, HttpStatusCode expected) { // Arrange - var document = new ErrorDocument(errorCodes.Select(code => new Error(code))); + var document = new Document + { + Errors = errorCodes.Select(code => new ErrorObject(code)).ToList() + }; // Act HttpStatusCode status = document.GetErrorStatusCode(); diff --git a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs index 4da96708b0..49313b4364 100644 --- a/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs +++ b/test/UnitTests/Internal/RequestScopedServiceProviderTests.cs @@ -24,7 +24,7 @@ public void When_http_context_is_unavailable_it_must_fail() // Assert var exception = Assert.Throws(action); - Assert.StartsWith("Cannot resolve scoped service " + $"'{serviceType.FullName}' outside the context of an HTTP request.", exception.Message); + Assert.StartsWith($"Cannot resolve scoped service '{serviceType.FullName}' outside the context of an HTTP request.", exception.Message); } [UsedImplicitly(ImplicitUseTargetFlags.Itself)] diff --git a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs index 2c2b391a8b..d1df3947d8 100644 --- a/test/UnitTests/Internal/ResourceGraphBuilderTests.cs +++ b/test/UnitTests/Internal/ResourceGraphBuilderTests.cs @@ -57,10 +57,10 @@ public void GetResourceContext_Yields_Right_Type_For_LazyLoadingProxy() // Act var proxy = proxyGenerator.CreateClassProxy(); - ResourceContext result = resourceGraph.GetResourceContext(proxy.GetType()); + ResourceContext resourceContext = resourceGraph.GetResourceContext(proxy.GetType()); // Assert - Assert.Equal(typeof(Bar), result.ResourceType); + Assert.Equal(typeof(Bar), resourceContext.ResourceType); } [Fact] @@ -72,10 +72,10 @@ public void GetResourceContext_Yields_Right_Type_For_Identifiable() var resourceGraph = (ResourceGraph)resourceGraphBuilder.Build(); // Act - ResourceContext result = resourceGraph.GetResourceContext(typeof(Bar)); + ResourceContext resourceContext = resourceGraph.GetResourceContext(typeof(Bar)); // Assert - Assert.Equal(typeof(Bar), result.ResourceType); + Assert.Equal(typeof(Bar), resourceContext.ResourceType); } [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] diff --git a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs index 22c4f92f57..93122f9a99 100644 --- a/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs +++ b/test/UnitTests/Middleware/JsonApiMiddlewareTests.cs @@ -79,7 +79,7 @@ private Task RunMiddlewareTask(InvokeConfiguration holder) { IControllerResourceMapping controllerResourceMapping = holder.ControllerResourceMapping.Object; HttpContext context = holder.HttpContext; - IJsonApiOptions options = holder.Options.Object; + IJsonApiOptions options = holder.Options; JsonApiRequest request = holder.Request; IResourceGraph resourceGraph = holder.ResourceGraph.Object; return holder.MiddleWare.InvokeAsync(context, controllerResourceMapping, options, request, resourceGraph, NullLogger.Instance); @@ -101,7 +101,7 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = var mockMapping = new Mock(); mockMapping.Setup(mapping => mapping.GetResourceTypeForController(It.IsAny())).Returns(typeof(string)); - Mock mockOptions = CreateMockOptions(forcedNamespace); + IJsonApiOptions options = CreateOptions(forcedNamespace); Mock mockGraph = CreateMockResourceGraph(resourceName, relType != null); var request = new JsonApiRequest(); @@ -119,18 +119,21 @@ private InvokeConfiguration GetConfiguration(string path, string resourceName = { MiddleWare = middleware, ControllerResourceMapping = mockMapping, - Options = mockOptions, + Options = options, Request = request, HttpContext = context, ResourceGraph = mockGraph }; } - private static Mock CreateMockOptions(string forcedNamespace) + private static IJsonApiOptions CreateOptions(string forcedNamespace) { - var mockOptions = new Mock(); - mockOptions.Setup(options => options.Namespace).Returns(forcedNamespace); - return mockOptions; + var options = new JsonApiOptions + { + Namespace = forcedNamespace + }; + + return options; } private static DefaultHttpContext CreateHttpContext(string path, bool isRelationship = false, string action = "", string id = null) @@ -190,7 +193,7 @@ private sealed class InvokeConfiguration public JsonApiMiddleware MiddleWare { get; init; } public HttpContext HttpContext { get; init; } public Mock ControllerResourceMapping { get; init; } - public Mock Options { get; init; } + public IJsonApiOptions Options { get; init; } public JsonApiRequest Request { get; init; } public Mock ResourceGraph { get; init; } } diff --git a/test/UnitTests/Middleware/JsonApiRequestTests.cs b/test/UnitTests/Middleware/JsonApiRequestTests.cs index c1b172662f..bb6a230fd7 100644 --- a/test/UnitTests/Middleware/JsonApiRequestTests.cs +++ b/test/UnitTests/Middleware/JsonApiRequestTests.cs @@ -52,6 +52,7 @@ public async Task Sets_request_properties_correctly(string requestMethod, string var graphBuilder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance); graphBuilder.Add(); graphBuilder.Add(); + graphBuilder.Add(); IResourceGraph resourceGraph = graphBuilder.Build(); diff --git a/test/UnitTests/Models/RelationshipDataTests.cs b/test/UnitTests/Models/RelationshipDataTests.cs deleted file mode 100644 index 278c158028..0000000000 --- a/test/UnitTests/Models/RelationshipDataTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json.Linq; -using Xunit; - -namespace UnitTests.Models -{ - public sealed class RelationshipDataTests - { - [Fact] - public void Setting_ExposeData_To_List_Sets_ManyData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - var relationships = new List - { - new() - { - Id = "9", - Type = "authors" - } - }; - - // Act - relationshipData.Data = relationships; - - // Assert - Assert.NotEmpty(relationshipData.ManyData); - Assert.Equal("authors", relationshipData.ManyData[0].Type); - Assert.Equal("9", relationshipData.ManyData[0].Id); - Assert.True(relationshipData.IsManyData); - } - - [Fact] - public void Setting_ExposeData_To_JArray_Sets_ManyData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - const string relationshipsJson = @"[ - { - ""type"": ""authors"", - ""id"": ""9"" - } - ]"; - - JArray relationships = JArray.Parse(relationshipsJson); - - // Act - relationshipData.Data = relationships; - - // Assert - Assert.NotEmpty(relationshipData.ManyData); - Assert.Equal("authors", relationshipData.ManyData[0].Type); - Assert.Equal("9", relationshipData.ManyData[0].Id); - Assert.True(relationshipData.IsManyData); - } - - [Fact] - public void Setting_ExposeData_To_RIO_Sets_SingleData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - var relationship = new ResourceIdentifierObject - { - Id = "9", - Type = "authors" - }; - - // Act - relationshipData.Data = relationship; - - // Assert - Assert.NotNull(relationshipData.SingleData); - Assert.Equal("authors", relationshipData.SingleData.Type); - Assert.Equal("9", relationshipData.SingleData.Id); - Assert.False(relationshipData.IsManyData); - } - - [Fact] - public void Setting_ExposeData_To_JObject_Sets_SingleData() - { - // Arrange - var relationshipData = new RelationshipEntry(); - - const string relationshipJson = @"{ - ""id"": ""9"", - ""type"": ""authors"" - }"; - - JObject relationship = JObject.Parse(relationshipJson); - - // Act - relationshipData.Data = relationship; - - // Assert - Assert.NotNull(relationshipData.SingleData); - Assert.Equal("authors", relationshipData.SingleData.Type); - Assert.Equal("9", relationshipData.SingleData.Id); - Assert.False(relationshipData.IsManyData); - } - } -} diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index 82a152dd41..81d08fe548 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.Design; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; @@ -7,7 +8,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; using Moq; -using Newtonsoft.Json; using Xunit; namespace UnitTests.Models @@ -32,13 +32,13 @@ public void When_resource_has_default_constructor_it_must_succeed() // Arrange var options = new JsonApiOptions(); - IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); + var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), + _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -49,7 +49,7 @@ public void When_resource_has_default_constructor_it_must_succeed() } }; - string content = JsonConvert.SerializeObject(body); + string content = JsonSerializer.Serialize(body); // Act object result = serializer.Deserialize(content); @@ -65,13 +65,13 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() // Arrange var options = new JsonApiOptions(); - IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); + var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), + _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -82,7 +82,7 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() } }; - string content = JsonConvert.SerializeObject(body); + string content = JsonSerializer.Serialize(body); // Act Action action = () => serializer.Deserialize(content); @@ -100,13 +100,13 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() // Arrange var options = new JsonApiOptions(); - IResourceGraph graph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); + IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(IResourceDefinitionAccessor), new NeverResourceDefinitionAccessor()); - var serializer = new RequestDeserializer(graph, new ResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object, - _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); + var serializer = new RequestDeserializer(resourceGraph, new ResourceFactory(serviceContainer), new TargetedFields(), + _mockHttpContextAccessor.Object, _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); var body = new { @@ -117,7 +117,7 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() } }; - string content = JsonConvert.SerializeObject(body); + string content = JsonSerializer.Serialize(body); // Act Action action = () => serializer.Deserialize(content); diff --git a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs b/test/UnitTests/Serialization/Client/RequestSerializerTests.cs deleted file mode 100644 index 96073b6645..0000000000 --- a/test/UnitTests/Serialization/Client/RequestSerializerTests.cs +++ /dev/null @@ -1,298 +0,0 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Building; -using JsonApiDotNetCore.Serialization.Client.Internal; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Client -{ - public sealed class RequestSerializerTests : SerializerTestsSetup - { - private readonly RequestSerializer _serializer; - - public RequestSerializerTests() - { - var builder = new ResourceObjectBuilder(ResourceGraph, new ResourceObjectBuilderSettings()); - _serializer = new RequestSerializer(ResourceGraph, builder); - } - - [Fact] - public void SerializeSingle_ResourceWithDefaultTargetFields_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - // Act - string serialized = _serializer.Serialize(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""id"":""1"", - ""attributes"":{ - ""stringField"":""value"", - ""dateTimeField"":""0001-01-01T00:00:00"", - ""nullableDateTimeField"":null, - ""intField"":0, - ""nullableIntField"":123, - ""guidField"":""00000000-0000-0000-0000-000000000000"", - ""complexField"":null - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithTargetedSetAttributes_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""id"":""1"", - ""attributes"":{ - ""stringField"":""value"" - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_NoIdWithTargetedSetAttributes_CanBuild() - { - // Arrange - var resourceNoId = new TestResource - { - Id = 0, - StringField = "value", - NullableIntField = 123 - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(resourceNoId); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""attributes"":{ - ""stringField"":""value"" - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithoutTargetedAttributes_CanBuild() - { - // Arrange - var resource = new TestResource - { - Id = 1, - StringField = "value", - NullableIntField = 123 - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(_ => new - { - }); - - // Act - string serialized = _serializer.Serialize(resource); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""testResource"", - ""id"":""1"" - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_ResourceWithTargetedRelationships_CanBuild() - { - // Arrange - var resourceWithRelationships = new MultipleRelationshipsPrincipalPart - { - PopulatedToOne = new OneToOneDependent - { - Id = 10 - }, - PopulatedToManies = new HashSet - { - new() - { - Id = 20 - } - } - }; - - _serializer.RelationshipsToSerialize = ResourceGraph.GetRelationships(tr => new - { - tr.EmptyToOne, - tr.EmptyToManies, - tr.PopulatedToOne, - tr.PopulatedToManies - }); - - // Act - string serialized = _serializer.Serialize(resourceWithRelationships); - - // Assert - const string expectedFormatted = @"{ - ""data"":{ - ""type"":""multiPrincipals"", - ""attributes"":{ - ""attributeMember"":null - }, - ""relationships"":{ - ""emptyToOne"":{ - ""data"":null - }, - ""emptyToManies"":{ - ""data"":[ ] - }, - ""populatedToOne"":{ - ""data"":{ - ""type"":""oneToOneDependents"", - ""id"":""10"" - } - }, - ""populatedToManies"":{ - ""data"":[ - { - ""type"":""oneToManyDependents"", - ""id"":""20"" - } - ] - } - } - } - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeMany_ResourcesWithTargetedAttributes_CanBuild() - { - // Arrange - var resources = new List - { - new() - { - Id = 1, - StringField = "value1", - NullableIntField = 123 - }, - new() - { - Id = 2, - StringField = "value2", - NullableIntField = 123 - } - }; - - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(resources); - - // Assert - const string expectedFormatted = @"{ - ""data"":[ - { - ""type"":""testResource"", - ""id"":""1"", - ""attributes"":{ - ""stringField"":""value1"" - } - }, - { - ""type"":""testResource"", - ""id"":""2"", - ""attributes"":{ - ""stringField"":""value2"" - } - } - ] - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeSingle_Null_CanBuild() - { - // Arrange - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize((IIdentifiable)null); - - // Assert - const string expectedFormatted = @"{ - ""data"":null - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - - [Fact] - public void SerializeMany_EmptyList_CanBuild() - { - // Arrange - _serializer.AttributesToSerialize = ResourceGraph.GetAttributes(tr => tr.StringField); - - // Act - string serialized = _serializer.Serialize(new List()); - - // Assert - const string expectedFormatted = @"{ - ""data"":[] - }"; - - string expected = Regex.Replace(expectedFormatted, @"\s+", ""); - Assert.Equal(expected, serialized); - } - } -} diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs deleted file mode 100644 index 4c494d0b31..0000000000 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ /dev/null @@ -1,483 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.Design; -using System.Linq; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Client.Internal; -using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; -using UnitTests.TestModels; -using Xunit; - -namespace UnitTests.Serialization.Client -{ - public sealed class ResponseDeserializerTests : DeserializerTestsSetup - { - private readonly Dictionary _linkValues = new(); - private readonly ResponseDeserializer _deserializer; - - public ResponseDeserializerTests() - { - _deserializer = new ResponseDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer())); - _linkValues.Add("self", "http://example.com/articles"); - _linkValues.Add("next", "http://example.com/articles?page[number]=2"); - _linkValues.Add("last", "http://example.com/articles?page[number]=10"); - } - - [Fact] - public void DeserializeSingle_EmptyResponseWithMeta_CanDeserialize() - { - // Arrange - var content = new Document - { - Meta = new Dictionary - { - ["foo"] = "bar" - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - - // Assert - Assert.Null(result.Data); - Assert.NotNull(result.Meta); - Assert.Equal("bar", result.Meta["foo"]); - } - - [Fact] - public void DeserializeSingle_EmptyResponseWithTopLevelLinks_CanDeserialize() - { - // Arrange - var content = new Document - { - Links = new TopLevelLinks - { - Self = _linkValues["self"], - Next = _linkValues["next"], - Last = _linkValues["last"] - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - - // Assert - Assert.Null(result.Data); - Assert.NotNull(result.Links); - TopLevelLinks links = result.Links; - Assert.Equal(_linkValues["self"], links.Self); - Assert.Equal(_linkValues["next"], links.Next); - Assert.Equal(_linkValues["last"], links.Last); - } - - [Fact] - public void DeserializeList_EmptyResponseWithTopLevelLinks_CanDeserialize() - { - // Arrange - var content = new Document - { - Links = new TopLevelLinks - { - Self = _linkValues["self"], - Next = _linkValues["next"], - Last = _linkValues["last"] - }, - Data = new List() - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - ManyResponse result = _deserializer.DeserializeMany(body); - - // Assert - Assert.Empty(result.Data); - Assert.NotNull(result.Links); - TopLevelLinks links = result.Links; - Assert.Equal(_linkValues["self"], links.Self); - Assert.Equal(_linkValues["next"], links.Next); - Assert.Equal(_linkValues["last"], links.Last); - } - - [Fact] - public void DeserializeSingle_ResourceWithAttributes_CanDeserialize() - { - // Arrange - Document content = CreateTestResourceDocument(); - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - TestResource resource = result.Data; - - // Assert - Assert.Null(result.Links); - Assert.Null(result.Meta); - Assert.Equal(1, resource.Id); - Assert.Equal(content.SingleData.Attributes["stringField"], resource.StringField); - } - - [Fact] - public void DeserializeSingle_MultipleDependentRelationshipsWithIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOneDependents")); - content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); - const string toOneAttributeValue = "populatedToOne member content"; - const string toManyAttributeValue = "populatedToManies member content"; - - content.Included = new List - { - new() - { - Type = "oneToOneDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toOneAttributeValue - } - }, - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toManyAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsPrincipalPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.NotNull(resource.PopulatedToOne); - Assert.Equal(toOneAttributeValue, resource.PopulatedToOne.AttributeMember); - Assert.Equal(toManyAttributeValue, resource.PopulatedToManies.First().AttributeMember); - Assert.NotNull(resource.PopulatedToManies); - Assert.NotNull(resource.EmptyToManies); - Assert.Empty(resource.EmptyToManies); - Assert.Null(resource.EmptyToOne); - } - - [Fact] - public void DeserializeSingle_MultiplePrincipalRelationshipsWithIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiDependents"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOnePrincipals")); - content.SingleData.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("emptyToMany", CreateRelationshipData()); - const string toOneAttributeValue = "populatedToOne member content"; - const string toManyAttributeValue = "populatedToManies member content"; - - content.Included = new List - { - new() - { - Type = "oneToOnePrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toOneAttributeValue - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toManyAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsDependentPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.NotNull(resource.PopulatedToOne); - Assert.Equal(toOneAttributeValue, resource.PopulatedToOne.AttributeMember); - Assert.Equal(toManyAttributeValue, resource.PopulatedToMany.AttributeMember); - Assert.NotNull(resource.PopulatedToMany); - Assert.Null(resource.EmptyToMany); - Assert.Null(resource.EmptyToOne); - } - - [Fact] - public void DeserializeSingle_NestedIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); - const string toManyAttributeValue = "populatedToManies member content"; - const string nestedIncludeAttributeValue = "nested include member content"; - - content.Included = new List - { - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = toManyAttributeValue - }, - Relationships = new Dictionary - { - ["principal"] = CreateRelationshipData("oneToManyPrincipals") - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = nestedIncludeAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsPrincipalPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.Null(resource.PopulatedToOne); - Assert.Null(resource.EmptyToManies); - Assert.Null(resource.EmptyToOne); - Assert.NotNull(resource.PopulatedToManies); - OneToManyDependent includedResource = resource.PopulatedToManies.First(); - Assert.Equal(toManyAttributeValue, includedResource.AttributeMember); - OneToManyPrincipal nestedIncludedResource = includedResource.Principal; - Assert.Equal(nestedIncludeAttributeValue, nestedIncludedResource.AttributeMember); - } - - [Fact] - public void DeserializeSingle_DeeplyNestedIncluded_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("multi", CreateRelationshipData("multiPrincipals")); - const string includedAttributeValue = "multi member content"; - const string nestedIncludedAttributeValue = "nested include member content"; - const string deeplyNestedIncludedAttributeValue = "deeply nested member content"; - - content.Included = new List - { - new() - { - Type = "multiPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = includedAttributeValue - }, - Relationships = new Dictionary - { - ["populatedToManies"] = CreateRelationshipData("oneToManyDependents", true) - } - }, - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = nestedIncludedAttributeValue - }, - Relationships = new Dictionary - { - ["principal"] = CreateRelationshipData("oneToManyPrincipals") - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = deeplyNestedIncludedAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - MultipleRelationshipsPrincipalPart resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - MultipleRelationshipsPrincipalPart included = resource.Multi; - Assert.Equal(10, included.Id); - Assert.Equal(includedAttributeValue, included.AttributeMember); - OneToManyDependent nestedIncluded = included.PopulatedToManies.First(); - Assert.Equal(10, nestedIncluded.Id); - Assert.Equal(nestedIncludedAttributeValue, nestedIncluded.AttributeMember); - OneToManyPrincipal deeplyNestedIncluded = nestedIncluded.Principal; - Assert.Equal(10, deeplyNestedIncluded.Id); - Assert.Equal(deeplyNestedIncludedAttributeValue, deeplyNestedIncluded.AttributeMember); - } - - [Fact] - public void DeserializeList_DeeplyNestedIncluded_CanDeserialize() - { - // Arrange - var content = new Document - { - Data = new List - { - CreateDocumentWithRelationships("multiPrincipals").SingleData - } - }; - - content.ManyData[0].Relationships.Add("multi", CreateRelationshipData("multiPrincipals")); - const string includedAttributeValue = "multi member content"; - const string nestedIncludedAttributeValue = "nested include member content"; - const string deeplyNestedIncludedAttributeValue = "deeply nested member content"; - - content.Included = new List - { - new() - { - Type = "multiPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = includedAttributeValue - }, - Relationships = new Dictionary - { - ["populatedToManies"] = CreateRelationshipData("oneToManyDependents", true) - } - }, - new() - { - Type = "oneToManyDependents", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = nestedIncludedAttributeValue - }, - Relationships = new Dictionary - { - ["principal"] = CreateRelationshipData("oneToManyPrincipals") - } - }, - new() - { - Type = "oneToManyPrincipals", - Id = "10", - Attributes = new Dictionary - { - ["attributeMember"] = deeplyNestedIncludedAttributeValue - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - ManyResponse result = _deserializer.DeserializeMany(body); - MultipleRelationshipsPrincipalPart resource = result.Data.First(); - - // Assert - Assert.Equal(1, resource.Id); - MultipleRelationshipsPrincipalPart included = resource.Multi; - Assert.Equal(10, included.Id); - Assert.Equal(includedAttributeValue, included.AttributeMember); - OneToManyDependent nestedIncluded = included.PopulatedToManies.First(); - Assert.Equal(10, nestedIncluded.Id); - Assert.Equal(nestedIncludedAttributeValue, nestedIncluded.AttributeMember); - OneToManyPrincipal deeplyNestedIncluded = nestedIncluded.Principal; - Assert.Equal(10, deeplyNestedIncluded.Id); - Assert.Equal(deeplyNestedIncludedAttributeValue, deeplyNestedIncluded.AttributeMember); - } - - [Fact] - public void DeserializeSingle_ResourceWithInheritanceAndInclusions_CanDeserialize() - { - // Arrange - Document content = CreateDocumentWithRelationships("testResourceWithAbstractRelationships"); - content.SingleData.Relationships.Add("toMany", CreateRelationshipData("firstDerivedModels", true)); - content.SingleData.Relationships["toMany"].ManyData.Add(CreateRelationshipData("secondDerivedModels", id: "11").SingleData); - content.SingleData.Relationships.Add("toOne", CreateRelationshipData("firstDerivedModels", id: "20")); - - content.Included = new List - { - new() - { - Type = "firstDerivedModels", - Id = "10", - Attributes = new Dictionary - { - ["firstProperty"] = "true" - } - }, - new() - { - Type = "secondDerivedModels", - Id = "11", - Attributes = new Dictionary - { - ["secondProperty"] = "false" - } - }, - new() - { - Type = "firstDerivedModels", - Id = "20", - Attributes = new Dictionary - { - ["firstProperty"] = "true" - } - } - }; - - string body = JsonConvert.SerializeObject(content); - - // Act - SingleResponse result = _deserializer.DeserializeSingle(body); - TestResourceWithAbstractRelationship resource = result.Data; - - // Assert - Assert.Equal(1, resource.Id); - Assert.NotNull(resource.ToOne); - Assert.True(resource.ToOne is FirstDerivedModel); - Assert.True(((FirstDerivedModel)resource.ToOne).FirstProperty); - - Assert.NotEmpty(resource.ToMany); - BaseModel first = resource.ToMany[0]; - Assert.True(first is FirstDerivedModel); - Assert.True(((FirstDerivedModel)first).FirstProperty); - - BaseModel second = resource.ToMany[1]; - Assert.True(second is SecondDerivedModel); - Assert.False(((SecondDerivedModel)second).SecondProperty); - } - } -} diff --git a/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs index e27938e1d3..5e590a4e21 100644 --- a/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/BaseDocumentBuilderTests.cs @@ -31,8 +31,8 @@ public void ResourceToDocument_NullResource_CanBuild() Document document = _builder.PublicBuild((TestResource)null); // Assert - Assert.Null(document.Data); - Assert.False(document.IsPopulated); + Assert.Null(document.Data.Value); + Assert.True(document.Data.IsAssigned); } [Fact] @@ -42,8 +42,8 @@ public void ResourceToDocument_EmptyList_CanBuild() Document document = _builder.PublicBuild(new List()); // Assert - Assert.NotNull(document.Data); - Assert.Empty(document.ManyData); + Assert.NotNull(document.Data.Value); + Assert.Empty(document.Data.ManyValue); } [Fact] @@ -56,8 +56,8 @@ public void ResourceToDocument_SingleResource_CanBuild() Document document = _builder.PublicBuild(dummy); // Assert - Assert.NotNull(document.Data); - Assert.True(document.IsPopulated); + Assert.NotNull(document.Data.Value); + Assert.True(document.Data.IsAssigned); } [Fact] @@ -68,7 +68,7 @@ public void ResourceToDocument_ResourceList_CanBuild() // Act Document document = _builder.PublicBuild(resources); - var data = (List)document.Data; + IList data = document.Data.ManyValue; // Assert Assert.Equal(2, data.Count); diff --git a/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs index a87890afdf..6e6b417af9 100644 --- a/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/BaseDocumentParserTests.cs @@ -3,10 +3,10 @@ using System.ComponentModel.Design; using System.Linq; using System.Reflection; +using System.Text.Json; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; using UnitTests.TestModels; using Xunit; @@ -18,7 +18,7 @@ public sealed class BaseDocumentParserTests : DeserializerTestsSetup public BaseDocumentParserTests() { - _deserializer = new TestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer())); + _deserializer = new TestDeserializer(ResourceGraph, new ResourceFactory(new ServiceContainer()), Options); } [Fact] @@ -27,14 +27,14 @@ public void DeserializeResourceIdentifiers_SingleData_CanDeserialize() // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1" - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (TestResource)_deserializer.Deserialize(body); @@ -48,7 +48,7 @@ public void DeserializeResourceIdentifiers_EmptySingleData_CanDeserialize() { // Arrange var content = new Document(); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act object result = _deserializer.Deserialize(body); @@ -63,17 +63,17 @@ public void DeserializeResourceIdentifiers_ArrayData_CanDeserialize() // Arrange var content = new Document { - Data = new List + Data = new SingleOrManyData(new List { new() { Type = "testResource", Id = "1" } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (IEnumerable)_deserializer.Deserialize(body); @@ -87,10 +87,10 @@ public void DeserializeResourceIdentifiers_EmptyArrayData_CanDeserialize() { var content = new Document { - Data = new List() + Data = new SingleOrManyData(Array.Empty()) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (IEnumerable)_deserializer.Deserialize(body); @@ -104,12 +104,12 @@ public void DeserializeResourceIdentifiers_EmptyArrayData_CanDeserialize() [InlineData("stringField", null)] [InlineData("intField", null, true)] [InlineData("intField", 1)] - [InlineData("intField", "1")] + [InlineData("intField", "1", true)] [InlineData("nullableIntField", null)] - [InlineData("nullableIntField", "1")] + [InlineData("nullableIntField", 1)] [InlineData("guidField", "bad format", true)] [InlineData("guidField", "1a68be43-cc84-4924-a421-7f4d614b7781")] - [InlineData("dateTimeField", "9/11/2019 11:41:40 AM")] + [InlineData("dateTimeField", "9/11/2019 11:41:40 AM", true)] [InlineData("dateTimeField", null, true)] [InlineData("nullableDateTimeField", null)] public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, object value, bool expectError = false) @@ -117,7 +117,7 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1", @@ -125,10 +125,10 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, { [member] = value } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act Func action = () => (TestResource)_deserializer.Deserialize(body); @@ -136,13 +136,13 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, // Assert if (expectError) { - Assert.ThrowsAny(action); + Assert.ThrowsAny(action); } else { TestResource resource = action(); - PropertyInfo pi = ResourceGraph.GetResourceContext("testResource").Attributes.Single(attr => attr.PublicName == member).Property; + PropertyInfo pi = ResourceGraph.GetResourceContext("testResource").GetAttributeByPublicName(member).Property; object deserializedValue = pi.GetValue(resource); if (member == "intField") @@ -153,7 +153,7 @@ public void DeserializeAttributes_VariousDataTypes_CanDeserialize(string member, { Assert.Null(deserializedValue); } - else if (member == "nullableIntField" && (string)value == "1") + else if (member == "nullableIntField" && (int?)value == 1) { Assert.Equal(1, deserializedValue); } @@ -178,7 +178,7 @@ public void DeserializeAttributes_ComplexType_CanDeserialize() // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1", @@ -189,10 +189,10 @@ public void DeserializeAttributes_ComplexType_CanDeserialize() ["compoundName"] = "testName" } } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (TestResource)_deserializer.Deserialize(body); @@ -208,7 +208,7 @@ public void DeserializeAttributes_ComplexListType_CanDeserialize() // Arrange var content = new Document { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource-with-list", Id = "1", @@ -222,10 +222,10 @@ public void DeserializeAttributes_ComplexListType_CanDeserialize() } } } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (TestResourceWithList)_deserializer.Deserialize(body); @@ -242,16 +242,16 @@ public void DeserializeRelationship_SingleDataForToOneRelationship_CannotDeseria // Arrange Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents"); - content.SingleData.Relationships["dependents"] = new RelationshipEntry + content.Data.SingleValue.Relationships["dependents"] = new RelationshipObject { - Data = new ResourceIdentifierObject + Data = new SingleOrManyData(new ResourceIdentifierObject { Type = "Dependents", Id = "1" - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act Action action = () => _deserializer.Deserialize(body); @@ -266,19 +266,19 @@ public void DeserializeRelationship_ManyDataForToManyRelationship_CannotDeserial // Arrange Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent"); - content.SingleData.Relationships["dependent"] = new RelationshipEntry + content.Data.SingleValue.Relationships["dependent"] = new RelationshipObject { - Data = new List + Data = new SingleOrManyData(new List { new() { Type = "Dependent", Id = "1" } - } + }) }; - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act Action action = () => _deserializer.Deserialize(body); @@ -292,7 +292,7 @@ public void DeserializeRelationships_EmptyOneToOneDependent_NavigationPropertyIs { // Arrange Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOnePrincipal)_deserializer.Deserialize(body); @@ -308,7 +308,7 @@ public void DeserializeRelationships_PopulatedOneToOneDependent_NavigationProper { // Arrange Document content = CreateDocumentWithRelationships("oneToOnePrincipals", "dependent", "oneToOneDependents"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOnePrincipal)_deserializer.Deserialize(body); @@ -324,7 +324,7 @@ public void DeserializeRelationships_EmptyOneToOnePrincipal_NavigationIsNull() { // Arrange Document content = CreateDocumentWithRelationships("oneToOneDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOneDependent)_deserializer.Deserialize(body); @@ -339,7 +339,7 @@ public void DeserializeRelationships_EmptyRequiredOneToOnePrincipal_NavigationIs { // Arrange Document content = CreateDocumentWithRelationships("oneToOneRequiredDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOneRequiredDependent)_deserializer.Deserialize(body); @@ -354,7 +354,7 @@ public void DeserializeRelationships_PopulatedOneToOnePrincipal_NavigationIsPopu { // Arrange Document content = CreateDocumentWithRelationships("oneToOneDependents", "principal", "oneToOnePrincipals"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToOneDependent)_deserializer.Deserialize(body); @@ -371,7 +371,7 @@ public void DeserializeRelationships_EmptyOneToManyPrincipal_NavigationIsNull() { // Arrange Document content = CreateDocumentWithRelationships("oneToManyDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyDependent)_deserializer.Deserialize(body); @@ -387,7 +387,7 @@ public void DeserializeRelationships_EmptyOneToManyRequiredPrincipal_NavigationI { // Arrange Document content = CreateDocumentWithRelationships("oneToMany-requiredDependents", "principal"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyRequiredDependent)_deserializer.Deserialize(body); @@ -403,7 +403,7 @@ public void DeserializeRelationships_PopulatedOneToManyPrincipal_NavigationIsPop { // Arrange Document content = CreateDocumentWithRelationships("oneToManyDependents", "principal", "oneToManyPrincipals"); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyDependent)_deserializer.Deserialize(body); @@ -420,7 +420,7 @@ public void DeserializeRelationships_EmptyOneToManyDependent_NavigationIsNull() { // Arrange Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents", isToManyData: true); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyPrincipal)_deserializer.Deserialize(body); @@ -436,7 +436,7 @@ public void DeserializeRelationships_PopulatedOneToManyDependent_NavigationIsPop { // Arrange Document content = CreateDocumentWithRelationships("oneToManyPrincipals", "dependents", "oneToManyDependents", true); - string body = JsonConvert.SerializeObject(content); + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act var result = (OneToManyPrincipal)_deserializer.Deserialize(body); diff --git a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs index 61872c4945..3e44774876 100644 --- a/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Common/ResourceObjectBuilderTests.cs @@ -1,6 +1,6 @@ -using System.Collections; using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization.Building; using JsonApiDotNetCore.Serialization.Objects; @@ -15,7 +15,7 @@ public sealed class ResourceObjectBuilderTests : SerializerTestsSetup public ResourceObjectBuilderTests() { - _builder = new ResourceObjectBuilder(ResourceGraph, new ResourceObjectBuilderSettings()); + _builder = new ResourceObjectBuilder(ResourceGraph, new JsonApiOptions()); } [Fact] @@ -151,16 +151,16 @@ public void ResourceWithRelationshipsToResourceObject_WithIncludedRelationshipsA // Assert Assert.Equal(4, resourceObject.Relationships.Count); - Assert.Null(resourceObject.Relationships["emptyToOne"].Data); - Assert.Empty((IList)resourceObject.Relationships["emptyToManies"].Data); - var populatedToOneData = (ResourceIdentifierObject)resourceObject.Relationships["populatedToOne"].Data; + Assert.Null(resourceObject.Relationships["emptyToOne"].Data.SingleValue); + Assert.Empty(resourceObject.Relationships["emptyToManies"].Data.ManyValue); + ResourceIdentifierObject populatedToOneData = resourceObject.Relationships["populatedToOne"].Data.SingleValue; Assert.NotNull(populatedToOneData); Assert.Equal("10", populatedToOneData.Id); Assert.Equal("oneToOneDependents", populatedToOneData.Type); - var populatedToManiesData = (List)resourceObject.Relationships["populatedToManies"].Data; - Assert.Single(populatedToManiesData); - Assert.Equal("20", populatedToManiesData.First().Id); - Assert.Equal("oneToManyDependents", populatedToManiesData.First().Type); + IList populatedToManyData = resourceObject.Relationships["populatedToManies"].Data.ManyValue; + Assert.Single(populatedToManyData); + Assert.Equal("20", populatedToManyData.First().Id); + Assert.Equal("oneToManyDependents", populatedToManyData.First().Type); } [Fact] @@ -183,8 +183,8 @@ public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyWhileRe // Assert Assert.Single(resourceObject.Relationships); - Assert.NotNull(resourceObject.Relationships["principal"].Data); - var ro = (ResourceIdentifierObject)resourceObject.Relationships["principal"].Data; + Assert.NotNull(resourceObject.Relationships["principal"].Data.Value); + ResourceIdentifierObject ro = resourceObject.Relationships["principal"].Data.SingleValue; Assert.Equal("10", ro.Id); } @@ -204,7 +204,7 @@ public void ResourceWithRelationshipsToResourceObject_DeviatingForeignKeyAndNoNa ResourceObject resourceObject = _builder.Build(resource, relationships: relationships); // Assert - Assert.Null(resourceObject.Relationships["principal"].Data); + Assert.Null(resourceObject.Relationships["principal"].Data.Value); } [Fact] @@ -227,8 +227,8 @@ public void ResourceWithRequiredRelationshipsToResourceObject_DeviatingForeignKe // Assert Assert.Single(resourceObject.Relationships); - Assert.NotNull(resourceObject.Relationships["principal"].Data); - var ro = (ResourceIdentifierObject)resourceObject.Relationships["principal"].Data; + Assert.NotNull(resourceObject.Relationships["principal"].Data.SingleValue); + ResourceIdentifierObject ro = resourceObject.Relationships["principal"].Data.SingleValue; Assert.Equal("10", ro.Id); } } diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index bf851f0f0c..05fc33c886 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -1,9 +1,13 @@ +using System; using System.Collections.Generic; +using System.Globalization; +using System.Text.Json; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Objects; using Microsoft.AspNetCore.Http; using Moq; @@ -12,10 +16,16 @@ namespace UnitTests.Serialization { public class DeserializerTestsSetup : SerializationTestsSetupBase { + protected readonly JsonApiOptions Options = new(); + protected readonly JsonSerializerOptions SerializerWriteOptions; + protected Mock MockHttpContextAccessor { get; } protected DeserializerTestsSetup() { + Options.SerializerOptions.Converters.Add(new ResourceObjectConverter(ResourceGraph)); + + SerializerWriteOptions = ((IJsonApiOptions)Options).SerializerWriteOptions; MockHttpContextAccessor = new Mock(); MockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); } @@ -24,7 +34,7 @@ protected Document CreateDocumentWithRelationships(string primaryType, string re bool isToManyData = false) { Document content = CreateDocumentWithRelationships(primaryType); - content.SingleData.Relationships.Add(relationshipMemberName, CreateRelationshipData(relatedType, isToManyData)); + content.Data.SingleValue.Relationships.Add(relationshipMemberName, CreateRelationshipData(relatedType, isToManyData)); return content; } @@ -32,18 +42,18 @@ protected Document CreateDocumentWithRelationships(string primaryType) { return new() { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Id = "1", Type = primaryType, - Relationships = new Dictionary() - } + Relationships = new Dictionary() + }) }; } - protected RelationshipEntry CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") + protected RelationshipObject CreateRelationshipData(string relatedType = null, bool isToManyData = false, string id = "10") { - var entry = new RelationshipEntry(); + var relationshipObject = new RelationshipObject(); ResourceIdentifierObject rio = relatedType == null ? null @@ -55,21 +65,22 @@ protected RelationshipEntry CreateRelationshipData(string relatedType = null, bo if (isToManyData) { - entry.Data = relatedType != null ? rio.AsList() : new List(); + IList rios = relatedType != null ? rio.AsList() : Array.Empty(); + relationshipObject.Data = new SingleOrManyData(rios); } else { - entry.Data = rio; + relationshipObject.Data = new SingleOrManyData(rio); } - return entry; + return relationshipObject; } protected Document CreateTestResourceDocument() { return new() { - Data = new ResourceObject + Data = new SingleOrManyData(new ResourceObject { Type = "testResource", Id = "1", @@ -79,25 +90,28 @@ protected Document CreateTestResourceDocument() ["intField"] = 1, ["nullableIntField"] = null, ["guidField"] = "1a68be43-cc84-4924-a421-7f4d614b7781", - ["dateTimeField"] = "9/11/2019 11:41:40 AM" + ["dateTimeField"] = DateTime.Parse("9/11/2019 11:41:40 AM", CultureInfo.InvariantCulture) } - } + }) }; } protected sealed class TestDeserializer : BaseDeserializer { - public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory) + private readonly IJsonApiOptions _options; + + public TestDeserializer(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IJsonApiOptions options) : base(resourceGraph, resourceFactory) { + _options = options; } public object Deserialize(string body) { - return DeserializeBody(body); + return DeserializeData(body, _options.SerializerReadOptions); } - protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipEntry data = null) + protected override void AfterProcessField(IIdentifiable resource, ResourceFieldAttribute field, RelationshipObject data = null) { } } diff --git a/test/UnitTests/Serialization/SerializerTestsSetup.cs b/test/UnitTests/Serialization/SerializerTestsSetup.cs index 08ff89ad21..ccc5dae8e9 100644 --- a/test/UnitTests/Serialization/SerializerTestsSetup.cs +++ b/test/UnitTests/Serialization/SerializerTestsSetup.cs @@ -61,15 +61,13 @@ protected ResponseSerializer GetResponseSerializer(IEnumerable(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, jsonApiOptions); + return new ResponseSerializer(meta, link, includedBuilder, fieldsToSerialize, resourceObjectBuilder, resourceDefinitionAccessor, options); } protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumerable> inclusionChains = null, @@ -81,9 +79,10 @@ protected ResponseResourceObjectBuilder GetResponseResourceObjectBuilder(IEnumer IEnumerable includeConstraints = GetIncludeConstraints(inclusionChainArray); IIncludedResourceObjectBuilder includedBuilder = GetIncludedBuilder(inclusionChains != null); IEvaluatedIncludeCache evaluatedIncludeCache = GetEvaluatedIncludeCache(inclusionChainArray); + var options = new JsonApiOptions(); - return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), - GetSerializerSettingsProvider(), evaluatedIncludeCache); + return new ResponseResourceObjectBuilder(link, includedBuilder, includeConstraints, ResourceGraph, GetResourceDefinitionAccessor(), options, + evaluatedIncludeCache); } private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQueryString) @@ -92,17 +91,10 @@ private IIncludedResourceObjectBuilder GetIncludedBuilder(bool hasIncludeQuerySt ILinkBuilder linkBuilder = GetLinkBuilder(); IResourceDefinitionAccessor resourceDefinitionAccessor = GetResourceDefinitionAccessor(); IRequestQueryStringAccessor queryStringAccessor = new FakeRequestQueryStringAccessor(hasIncludeQueryString ? "include=" : null); - IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider(); + var options = new JsonApiOptions(); return new IncludedResourceObjectBuilder(fieldsToSerialize, linkBuilder, ResourceGraph, Enumerable.Empty(), - resourceDefinitionAccessor, queryStringAccessor, resourceObjectBuilderSettingsProvider); - } - - protected IResourceObjectBuilderSettingsProvider GetSerializerSettingsProvider() - { - var mock = new Mock(); - mock.Setup(provider => provider.Get()).Returns(new ResourceObjectBuilderSettings()); - return mock.Object; + resourceDefinitionAccessor, queryStringAccessor, options); } private IResourceDefinitionAccessor GetResourceDefinitionAccessor() diff --git a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs index ff9a2d5855..ee4fb88492 100644 --- a/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/IncludedResourceObjectBuilderTests.cs @@ -31,11 +31,11 @@ public void BuildIncluded_DeeplyNestedCircularChainOfSingleData_CanBuild() Assert.Equal(6, result.Count); ResourceObject authorResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == author.StringId); - ResourceIdentifierObject authorFoodRelation = authorResourceObject.Relationships["favoriteFood"].SingleData; + ResourceIdentifierObject authorFoodRelation = authorResourceObject.Relationships["favoriteFood"].Data.SingleValue; Assert.Equal(author.FavoriteFood.StringId, authorFoodRelation.Id); ResourceObject reviewerResourceObject = result.Single(ro => ro.Type == "people" && ro.Id == reviewer.StringId); - ResourceIdentifierObject reviewerFoodRelation = reviewerResourceObject.Relationships["favoriteFood"].SingleData; + ResourceIdentifierObject reviewerFoodRelation = reviewerResourceObject.Relationships["favoriteFood"].Data.SingleValue; Assert.Equal(reviewer.FavoriteFood.StringId, reviewerFoodRelation.Id); } @@ -111,7 +111,7 @@ public void BuildIncluded_DuplicateChildrenMultipleChains_OnceInOutput() IList result = builder.Build(); Assert.Single(result); Assert.Equal(person.Name, result[0].Attributes["name"]); - Assert.Equal(person.Id.ToString(), result[0].Id); + Assert.Equal(person.StringId, result[0].Id); } private Song GetReviewerChainInstances(Article article, Blog sharedBlog, Person sharedBlogAuthor) @@ -166,8 +166,7 @@ private IReadOnlyCollection GetIncludedRelationshipsChain foreach (string requestedRelationship in splitPath) { - RelationshipAttribute relationship = - resourceContext.Relationships.Single(nextRelationship => nextRelationship.PublicName == requestedRelationship); + RelationshipAttribute relationship = resourceContext.GetRelationshipByPublicName(requestedRelationship); parsedChain.Add(relationship); resourceContext = ResourceGraph.GetResourceContext(relationship.RightType); @@ -182,10 +181,10 @@ private IncludedResourceObjectBuilder GetBuilder() ILinkBuilder links = GetLinkBuilder(); IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock().Object; var queryStringAccessor = new FakeRequestQueryStringAccessor(); - IResourceObjectBuilderSettingsProvider resourceObjectBuilderSettingsProvider = GetSerializerSettingsProvider(); + var options = new JsonApiOptions(); return new IncludedResourceObjectBuilder(fields, links, ResourceGraph, Enumerable.Empty(), resourceDefinitionAccessor, - queryStringAccessor, resourceObjectBuilderSettingsProvider); + queryStringAccessor, options); } private sealed class AuthorChainInstances diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index e683e67c02..c6f8747eba 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -1,12 +1,13 @@ using System.Collections.Generic; +using System.Text.Json; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; +using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Objects; using Moq; -using Newtonsoft.Json; using Xunit; namespace UnitTests.Serialization.Server @@ -20,8 +21,11 @@ public sealed class RequestDeserializerTests : DeserializerTestsSetup public RequestDeserializerTests() { + var options = new JsonApiOptions(); + options.SerializerOptions.Converters.Add(new ResourceObjectConverter(ResourceGraph)); + _deserializer = new RequestDeserializer(ResourceGraph, new TestResourceFactory(), _fieldsManagerMock.Object, MockHttpContextAccessor.Object, - _requestMock.Object, new JsonApiOptions(), _resourceDefinitionAccessorMock.Object); + _requestMock.Object, options, _resourceDefinitionAccessorMock.Object); } [Fact] @@ -33,7 +37,8 @@ public void DeserializeAttributes_VariousUpdatedMembers_RegistersTargetedFields( SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); Document content = CreateTestResourceDocument(); - string body = JsonConvert.SerializeObject(content); + + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act _deserializer.Deserialize(body); @@ -52,11 +57,12 @@ public void DeserializeRelationships_MultipleDependentRelationships_RegistersUpd SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); Document content = CreateDocumentWithRelationships("multiPrincipals"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOneDependents")); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); - content.SingleData.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); - string body = JsonConvert.SerializeObject(content); + content.Data.SingleValue.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOneDependents")); + content.Data.SingleValue.Relationships.Add("emptyToOne", CreateRelationshipData()); + content.Data.SingleValue.Relationships.Add("populatedToManies", CreateRelationshipData("oneToManyDependents", true)); + content.Data.SingleValue.Relationships.Add("emptyToManies", CreateRelationshipData(isToManyData: true)); + + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act _deserializer.Deserialize(body); @@ -75,11 +81,12 @@ public void DeserializeRelationships_MultiplePrincipalRelationships_RegistersUpd SetupFieldsManager(attributesToUpdate, relationshipsToUpdate); Document content = CreateDocumentWithRelationships("multiDependents"); - content.SingleData.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOnePrincipals")); - content.SingleData.Relationships.Add("emptyToOne", CreateRelationshipData()); - content.SingleData.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); - content.SingleData.Relationships.Add("emptyToMany", CreateRelationshipData()); - string body = JsonConvert.SerializeObject(content); + content.Data.SingleValue.Relationships.Add("populatedToOne", CreateRelationshipData("oneToOnePrincipals")); + content.Data.SingleValue.Relationships.Add("emptyToOne", CreateRelationshipData()); + content.Data.SingleValue.Relationships.Add("populatedToMany", CreateRelationshipData("oneToManyPrincipals")); + content.Data.SingleValue.Relationships.Add("emptyToMany", CreateRelationshipData()); + + string body = JsonSerializer.Serialize(content, SerializerWriteOptions); // Act _deserializer.Deserialize(body); diff --git a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs index 183b23ed9b..e103cac808 100644 --- a/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseResourceObjectBuilderTests.cs @@ -23,7 +23,7 @@ public ResponseResourceObjectBuilderTests() } [Fact] - public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipEntryWithLinks() + public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipObjectWithLinks() { // Arrange var resource = new OneToManyPrincipal @@ -37,10 +37,10 @@ public void Build_RelationshipNotIncludedAndLinksEnabled_RelationshipEntryWithLi ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipEntry entry)); - Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); - Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); - Assert.False(entry.IsPopulated); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipObject relationshipObject)); + Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", relationshipObject.Links.Self); + Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", relationshipObject.Links.Related); + Assert.False(relationshipObject.Data.IsAssigned); } [Fact] @@ -62,7 +62,7 @@ public void Build_RelationshipNotIncludedAndLinksDisabled_NoRelationshipObject() } [Fact] - public void Build_RelationshipIncludedAndLinksDisabled_RelationshipEntryWithData() + public void Build_RelationshipIncludedAndLinksDisabled_RelationshipObjectWithData() { // Arrange var resource = new OneToManyPrincipal @@ -83,14 +83,14 @@ public void Build_RelationshipIncludedAndLinksDisabled_RelationshipEntryWithData ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipEntry entry)); - Assert.Null(entry.Links); - Assert.True(entry.IsPopulated); - Assert.Equal("20", entry.ManyData.Single().Id); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipObject relationshipObject)); + Assert.Null(relationshipObject.Links); + Assert.True(relationshipObject.Data.IsAssigned); + Assert.Equal("20", relationshipObject.Data.ManyValue.Single().Id); } [Fact] - public void Build_RelationshipIncludedAndLinksEnabled_RelationshipEntryWithDataAndLinks() + public void Build_RelationshipIncludedAndLinksEnabled_RelationshipObjectWithDataAndLinks() { // Arrange var resource = new OneToManyPrincipal @@ -112,11 +112,11 @@ public void Build_RelationshipIncludedAndLinksEnabled_RelationshipEntryWithDataA ResourceObject resourceObject = builder.Build(resource, relationships: _relationshipsForBuild); // Assert - Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipEntry entry)); - Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", entry.Links.Self); - Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", entry.Links.Related); - Assert.True(entry.IsPopulated); - Assert.Equal("20", entry.ManyData.Single().Id); + Assert.True(resourceObject.Relationships.TryGetValue(RelationshipName, out RelationshipObject relationshipObject)); + Assert.Equal("http://www.dummy.com/dummy-relationship-self-link", relationshipObject.Links.Self); + Assert.Equal("http://www.dummy.com/dummy-relationship-related-link", relationshipObject.Links.Related); + Assert.True(relationshipObject.Data.IsAssigned); + Assert.Equal("20", relationshipObject.Data.ManyValue.Single().Id); } } } diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs index 661781587c..2058c79e85 100644 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -1,13 +1,13 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text.Json; using System.Text.RegularExpressions; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; -using Newtonsoft.Json; using UnitTests.TestModels; using Xunit; @@ -368,14 +368,14 @@ public void SerializeSingle_ResourceWithMeta_IncludesMetaInResult() // Assert const string expectedFormatted = @"{ - ""meta"":{ ""test"": ""meta"" }, ""data"":{ ""type"":""oneToManyPrincipals"", ""id"":""10"", ""attributes"":{ ""attributeMember"":null } - } + }, + ""meta"":{ ""test"": ""meta"" } }"; string expected = Regex.Replace(expectedFormatted, @"\s+", ""); @@ -399,7 +399,6 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() // Assert const string expectedFormatted = @"{ - ""meta"":{ ""test"": ""meta"" }, ""links"":{ ""self"":""http://www.dummy.com/dummy-self-link"", ""first"":""http://www.dummy.com/dummy-first-link"", @@ -407,7 +406,8 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() ""prev"":""http://www.dummy.com/dummy-prev-link"", ""next"":""http://www.dummy.com/dummy-next-link"" }, - ""data"": null + ""data"": null, + ""meta"":{ ""test"": ""meta"" } }"; string expected = Regex.Replace(expectedFormatted, @"\s+", ""); @@ -418,15 +418,18 @@ public void SerializeSingle_NullWithLinksAndMeta_StillShowsLinksAndMeta() public void SerializeError_Error_CanSerialize() { // Arrange - var error = new Error(HttpStatusCode.InsufficientStorage) + var error = new ErrorObject(HttpStatusCode.InsufficientStorage) { Title = "title", Detail = "detail" }; - var errorDocument = new ErrorDocument(error); + var errorDocument = new Document + { + Errors = error.AsList() + }; - string expectedJson = JsonConvert.SerializeObject(new + string expectedJson = JsonSerializer.Serialize(new { errors = new[] {