Skip to content

Commit

Permalink
Use System.Text.Json (#1075)
Browse files Browse the repository at this point in the history
* Removed Serialization.Client.Internal

* Removed undocumented ?nulls and ?defaults query string support

* Refactor: use interpolated strings instead of concatenation

* Updated tests to use string value for IDs; centralized pseudo-constants

* Added tests for pascal casing

* Optimized attribute/relationship lookups

* Breaking: Made IResourceContextProvider.GetResourceContext() throw when not found; added TryGetResourceContext() that returns null

* Optimized resource graph lookups

* Breaking: Merged IResourceContextProvider into IResourceGraph

* Switched to STJ in assertions

Note we need JsonDateTimeOffsetFormatSpecifier now, because STJ never tries to infer the CLR type from JSON values between quotes, while Newtonsoft does. So Newtonsoft would convert both values to date/time, effectively hiding the textual difference that was always there.

* Switched to STJ in rendering exception stack traces

* Switched to STJ in rendering CLR objects as part of tracing. STJ properly handles self-referencing EF Core objects when enabling reference tracking, as opposed to Newtonsoft.

* Switched to STJ in attribute change tracking. This used to take options into account, which is unneeded because we only care about whether there's a diff, not so much what that diff looks like. And we don't expect self-references here (it would have crashed in the past, and will now too).

* Switched to STJ in Microservices example

* Removed re-indent of response body on HTTP status code mismatch in tests, because we already use indenting in TestableStartup, so this is no longer needed.

* Use STJ naming convention on special-cased code paths

* Renamed RelationshipEntry to RelationshipObject, Error to ErrorObject

* Fix broken test in cibuild

* Fixed broken tests in cibuild due to different line endings

* Package updates

* Refactor serialization objects
- Simplified error objects, so they are similar to the other serialization objects. This means no default instances, constructors (exception: ErrorObject) or conditional serialization logic. And explicit names to overrule naming conventions. And annotations to skip serialization when null.
- Added missing members from JSON:API v1.1 spec: ErrorDocument.Meta, ErrorLinks.Type, ErrorSource.Header, ResourceIdentifierObject.Meta
- Normalized collection types
- Updated documentation: Link to v1.1 of JSON:API spec instead of copy/pasted text

* Merged ErrorDocument and AtomicOperationsDocument into Document
Bugfix: jsonapi/version was missing in error responses

* Fill error.source.header where applicable

* Breaking: Renamed "total-resources" meta key to "total" because thats what Ember.js expects it to be named (see https://guides.emberjs.com/release/models/handling-metadata/)

* Removed unneeded StringEnumConverter usage. Also removed it from the defaults for tests, because that hides the problem when we forget to put it on a member that needs it.

* Use configured STJ options for null/default value inclusion
Bugfix: do not break out of method on first attribute

* Fixed data type in json request body

* Added missing type, which is a required element

* Converted core code to use System.Text.Json
- Added various converters to steer JsonSerializer in the right direction
- JsonApiDotNetCore.Serialization.Objects
  - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order
  - Moved "meta" to the end in all types (it is secondary information)
  - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore

* Updated documentation

* Fixed broken example-generation. Set launchBrowser to true, so it shows sample data on F5.

* Inlined properties on serializable objects

* Add test for incompatible ID value.
By default, this produces:
```
The JSON value could not be converted to JsonApiDotNetCore.Serialization.Objects.SingleOrManyData`1[JsonApiDotNetCore.Serialization.Objects.ResourceObject]. Path: $.data | LineNumber: 3 | BytePositionInLine: 11.
```
which is totally unhelpful. Because this is so likely to hit users, we special-case here to produce a better error.

* Removed misplaced launchsettings.json

* Review feedback: use base class instead of static helper
  • Loading branch information
Bart Koelman committed Dec 3, 2021
1 parent 00930cb commit 6d9e900
Show file tree
Hide file tree
Showing 313 changed files with 5,941 additions and 6,730 deletions.
9 changes: 5 additions & 4 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
<EFCoreVersion>5.0.*</EFCoreVersion>
<NpgsqlPostgreSQLVersion>5.0.*</NpgsqlPostgreSQLVersion>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
<WarningLevel>9999</WarningLevel>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" PrivateAssets="All" />
<PackageReference Include="CSharpGuidelinesAnalyzer" Version="3.6.0" PrivateAssets="All" />
<PackageReference Include="CSharpGuidelinesAnalyzer" Version="3.7.0" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CSharpGuidelinesAnalyzer.config" Visible="False" />
</ItemGroup>

Expand All @@ -21,11 +22,11 @@

<!-- Test Project Dependencies -->
<PropertyGroup>
<BogusVersion>33.0.2</BogusVersion>
<BogusVersion>33.1.1</BogusVersion>
<CoverletVersion>3.1.0</CoverletVersion>
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
<FluentAssertionsVersion>6.1.0</FluentAssertionsVersion>
<MoqVersion>4.16.1</MoqVersion>
<XUnitVersion>2.4.*</XUnitVersion>
<TestSdkVersion>16.10.0</TestSdkVersion>
<TestSdkVersion>16.11.0</TestSdkVersion>
</PropertyGroup>
</Project>
3 changes: 3 additions & 0 deletions benchmarks/DependencyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ internal sealed class DependencyFactory
public IResourceGraph CreateResourceGraph(IJsonApiOptions options)
{
var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);

builder.Add<BenchmarkResource>(BenchmarkResourcePublicNames.Type);
builder.Add<SubResource>();

return builder.Build();
}
}
Expand Down
4 changes: 1 addition & 3 deletions benchmarks/Query/QueryParserBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<IQueryStringParameterReader>(includeReader, filterReader, sortReader,
sparseFieldSetReader, paginationReader, defaultsReader, nullsReader);
sparseFieldSetReader, paginationReader);

return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance);
}
Expand Down
18 changes: 7 additions & 11 deletions benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
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
{
// ReSharper disable once ClassCanBeSealed.Global
[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<string, object>
type = BenchmarkResourcePublicNames.Type,
id = "1",
attributes = new
{
["name"] = Guid.NewGuid().ToString()
}
}
});
Expand Down Expand Up @@ -55,7 +51,7 @@ public JsonApiDeserializerBenchmarks()
[Benchmark]
public object DeserializeSimpleObject()
{
return _jsonApiDeserializer.Deserialize(Content);
return _jsonApiDeserializer.Deserialize(RequestBody);
}
}
}
2 changes: 1 addition & 1 deletion benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public JsonApiSerializerBenchmarks()
ILinkBuilder linkBuilder = new Mock<ILinkBuilder>().Object;
IIncludedResourceObjectBuilder includeBuilder = new Mock<IIncludedResourceObjectBuilder>().Object;

var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, options);

IResourceDefinitionAccessor resourceDefinitionAccessor = new Mock<IResourceDefinitionAccessor>().Object;

Expand Down
2 changes: 1 addition & 1 deletion docs/request-examples/012_PATCH_Book.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ curl -s -f http://localhost:14141/api/books/1 `
-d '{
\"data\": {
\"type\": \"books\",
\"id\": "1",
\"id\": \"1\",
\"attributes\": {
\"publishYear\": 1820
}
Expand Down
21 changes: 10 additions & 11 deletions docs/usage/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/resource-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
15 changes: 8 additions & 7 deletions docs/usage/resources/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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#
Expand Down Expand Up @@ -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
Expand All @@ -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; }
}
```
Expand All @@ -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<Bar>(value);
: JsonSerializer.Deserialize<Bar>(value);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/resources/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The left side of this relationship is of type `Article` (public name: "articles"

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#
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TResource>`, 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<TResource>`, the [configured naming convention](~/usage/options.md#customize-serializer-options) is applied to the name of the controller.

```c#
public class OrderLineController : ControllerBase
Expand Down
4 changes: 2 additions & 2 deletions src/Examples/GettingStarted/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": false,
"launchBrowser": true,
"launchUrl": "api/people",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Kestrel": {
"commandName": "Project",
"launchBrowser": false,
"launchBrowser": true,
"launchUrl": "api/people",
"applicationUrl": "http://localhost:14141",
"environmentVariables": {
Expand Down
3 changes: 1 addition & 2 deletions src/Examples/GettingStarted/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;

namespace GettingStarted
{
Expand All @@ -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;
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,21 @@ public async Task<IActionResult> 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);
}

Expand Down
7 changes: 3 additions & 4 deletions src/Examples/JsonApiDotNetCoreExample/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Text.Json.Serialization;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCoreExample.Data;
Expand All @@ -9,8 +10,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace JsonApiDotNetCoreExample
{
Expand Down Expand Up @@ -52,8 +51,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
Expand Down
8 changes: 4 additions & 4 deletions src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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."
Expand All @@ -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."
Expand All @@ -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}'."
Expand Down
Loading

0 comments on commit 6d9e900

Please sign in to comment.