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
///
- /// 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