Skip to content

Commit c933e8b

Browse files
author
Bart Koelman
authored
Use System.Text.Json (#1075)
* 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
1 parent 4b487f0 commit c933e8b

File tree

313 files changed

+5941
-6730
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

313 files changed

+5941
-6730
lines changed

Directory.Build.props

+5-4
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
<EFCoreVersion>5.0.*</EFCoreVersion>
66
<NpgsqlPostgreSQLVersion>5.0.*</NpgsqlPostgreSQLVersion>
77
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
8+
<WarningLevel>9999</WarningLevel>
89
</PropertyGroup>
910

1011
<ItemGroup>
1112
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" PrivateAssets="All" />
12-
<PackageReference Include="CSharpGuidelinesAnalyzer" Version="3.6.0" PrivateAssets="All" />
13+
<PackageReference Include="CSharpGuidelinesAnalyzer" Version="3.7.0" PrivateAssets="All" />
1314
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CSharpGuidelinesAnalyzer.config" Visible="False" />
1415
</ItemGroup>
1516

@@ -21,11 +22,11 @@
2122

2223
<!-- Test Project Dependencies -->
2324
<PropertyGroup>
24-
<BogusVersion>33.0.2</BogusVersion>
25+
<BogusVersion>33.1.1</BogusVersion>
2526
<CoverletVersion>3.1.0</CoverletVersion>
26-
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
27+
<FluentAssertionsVersion>6.1.0</FluentAssertionsVersion>
2728
<MoqVersion>4.16.1</MoqVersion>
2829
<XUnitVersion>2.4.*</XUnitVersion>
29-
<TestSdkVersion>16.10.0</TestSdkVersion>
30+
<TestSdkVersion>16.11.0</TestSdkVersion>
3031
</PropertyGroup>
3132
</Project>

benchmarks/DependencyFactory.cs

+3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ internal sealed class DependencyFactory
88
public IResourceGraph CreateResourceGraph(IJsonApiOptions options)
99
{
1010
var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);
11+
1112
builder.Add<BenchmarkResource>(BenchmarkResourcePublicNames.Type);
13+
builder.Add<SubResource>();
14+
1215
return builder.Build();
1316
}
1417
}

benchmarks/Query/QueryParserBenchmarks.cs

+1-3
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,9 @@ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGr
6464
var sortReader = new SortQueryStringParameterReader(request, resourceGraph);
6565
var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(request, resourceGraph);
6666
var paginationReader = new PaginationQueryStringParameterReader(request, resourceGraph, options);
67-
var defaultsReader = new DefaultsQueryStringParameterReader(options);
68-
var nullsReader = new NullsQueryStringParameterReader(options);
6967

7068
IQueryStringParameterReader[] readers = ArrayFactory.Create<IQueryStringParameterReader>(includeReader, filterReader, sortReader,
71-
sparseFieldSetReader, paginationReader, defaultsReader, nullsReader);
69+
sparseFieldSetReader, paginationReader);
7270

7371
return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance);
7472
}

benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
1-
using System;
2-
using System.Collections.Generic;
31
using System.ComponentModel.Design;
2+
using System.Text.Json;
43
using BenchmarkDotNet.Attributes;
54
using JsonApiDotNetCore.Configuration;
65
using JsonApiDotNetCore.Middleware;
76
using JsonApiDotNetCore.Resources;
87
using JsonApiDotNetCore.Serialization;
9-
using JsonApiDotNetCore.Serialization.Objects;
108
using Microsoft.AspNetCore.Http;
11-
using Newtonsoft.Json;
129

1310
namespace Benchmarks.Serialization
1411
{
1512
// ReSharper disable once ClassCanBeSealed.Global
1613
[MarkdownExporter]
1714
public class JsonApiDeserializerBenchmarks
1815
{
19-
private static readonly string Content = JsonConvert.SerializeObject(new Document
16+
private static readonly string RequestBody = JsonSerializer.Serialize(new
2017
{
21-
Data = new ResourceObject
18+
data = new
2219
{
23-
Type = BenchmarkResourcePublicNames.Type,
24-
Id = "1",
25-
Attributes = new Dictionary<string, object>
20+
type = BenchmarkResourcePublicNames.Type,
21+
id = "1",
22+
attributes = new
2623
{
27-
["name"] = Guid.NewGuid().ToString()
2824
}
2925
}
3026
});
@@ -55,7 +51,7 @@ public JsonApiDeserializerBenchmarks()
5551
[Benchmark]
5652
public object DeserializeSimpleObject()
5753
{
58-
return _jsonApiDeserializer.Deserialize(Content);
54+
return _jsonApiDeserializer.Deserialize(RequestBody);
5955
}
6056
}
6157
}

benchmarks/Serialization/JsonApiSerializerBenchmarks.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public JsonApiSerializerBenchmarks()
3434
ILinkBuilder linkBuilder = new Mock<ILinkBuilder>().Object;
3535
IIncludedResourceObjectBuilder includeBuilder = new Mock<IIncludedResourceObjectBuilder>().Object;
3636

37-
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, new ResourceObjectBuilderSettings());
37+
var resourceObjectBuilder = new ResourceObjectBuilder(resourceGraph, options);
3838

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

docs/request-examples/012_PATCH_Book.ps1

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ curl -s -f http://localhost:14141/api/books/1 `
44
-d '{
55
\"data\": {
66
\"type\": \"books\",
7-
\"id\": "1",
7+
\"id\": \"1\",
88
\"attributes\": {
99
\"publishYear\": 1820
1010
}

docs/usage/options.md

+10-11
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,26 @@ To limit the maximum depth of nested includes, use `MaximumIncludeDepth`. This i
7878
options.MaximumIncludeDepth = 1;
7979
```
8080

81-
## Custom Serializer Settings
81+
## Customize Serializer options
8282

83-
We use [Newtonsoft.Json](https://www.newtonsoft.com/json) for all serialization needs.
84-
If you want to change the default serializer settings, you can:
83+
We use [System.Text.Json](https://www.nuget.org/packages/System.Text.Json) for all serialization needs.
84+
If you want to change the default serializer options, you can:
8585

8686
```c#
87-
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
88-
options.SerializerSettings.Converters.Add(new StringEnumConverter());
89-
options.SerializerSettings.Formatting = Formatting.Indented;
87+
options.SerializerOptions.WriteIndented = true;
88+
options.SerializerOptions.ReferenceHandler = ReferenceHandler.Preserve;
89+
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
9090
```
9191

9292
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):
9393

9494
```c#
95-
options.SerializerSettings.ContractResolver = new DefaultContractResolver
96-
{
97-
NamingStrategy = new KebabCaseNamingStrategy()
98-
};
95+
// Use Pascal case
96+
options.SerializerOptions.PropertyNamingPolicy = null;
97+
options.SerializerOptions.DictionaryKeyPolicy = null;
9998
```
10099

101-
Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored.
100+
Because we copy resource properties into an intermediate object before serialization, JSON annotations such as `[JsonPropertyName]` and `[JsonIgnore]` on `[Attr]` properties are ignored.
102101

103102

104103
## Enable ModelState Validation

docs/usage/resource-graph.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,4 @@ public class MyModel : Identifiable
9898
}
9999
```
100100

101-
The default naming convention can be changed in [options](~/usage/options.md#custom-serializer-settings).
101+
The default naming convention can be changed in [options](~/usage/options.md#customize-serializer-options).

docs/usage/resources/attributes.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class Person : Identifiable
1414

1515
There are two ways the exposed attribute name is determined:
1616

17-
1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings).
17+
1. Using the configured [naming convention](~/usage/options.md#customize-serializer-options).
1818

1919
2. Individually using the attribute's constructor.
2020
```c#
@@ -88,9 +88,9 @@ public class Person : Identifiable
8888
## Complex Attributes
8989

9090
Models may contain complex attributes.
91-
Serialization of these types is done by [Newtonsoft.Json](https://www.newtonsoft.com/json),
92-
so you should use their APIs to specify serialization formats.
93-
You can also use global options to specify `JsonSerializer` configuration.
91+
Serialization of these types is done by [System.Text.Json](https://www.nuget.org/packages/System.Text.Json),
92+
so you should use their APIs to specify serialization format.
93+
You can also use [global options](~/usage/options.md#customize-serializer-options) to control the `JsonSerializer` behavior.
9494

9595
```c#
9696
public class Foo : Identifiable
@@ -101,7 +101,8 @@ public class Foo : Identifiable
101101

102102
public class Bar
103103
{
104-
[JsonProperty("compound-member")]
104+
[JsonPropertyName("compound-member")]
105+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
105106
public string CompoundMember { get; set; }
106107
}
107108
```
@@ -121,13 +122,13 @@ public class Foo : Identifiable
121122
{
122123
get
123124
{
124-
return Bar == null ? "{}" : JsonConvert.SerializeObject(Bar);
125+
return Bar == null ? "{}" : JsonSerializer.Serialize(Bar);
125126
}
126127
set
127128
{
128129
Bar = string.IsNullOrWhiteSpace(value)
129130
? null
130-
: JsonConvert.DeserializeObject<Bar>(value);
131+
: JsonSerializer.Deserialize<Bar>(value);
131132
}
132133
}
133134
}

docs/usage/resources/relationships.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The left side of this relationship is of type `Article` (public name: "articles"
6464

6565
There are two ways the exposed relationship name is determined:
6666

67-
1. Using the configured [naming convention](~/usage/options.md#custom-serializer-settings).
67+
1. Using the configured [naming convention](~/usage/options.md#customize-serializer-options).
6868

6969
2. Individually using the attribute's constructor.
7070
```c#

docs/usage/routing.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ The exposed name of the resource ([which can be customized](~/usage/resource-gra
4545

4646
### Non-JSON:API controllers
4747

48-
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.
48+
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.
4949

5050
```c#
5151
public class OrderLineController : ControllerBase

src/Examples/GettingStarted/Properties/launchSettings.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
"profiles": {
1111
"IIS Express": {
1212
"commandName": "IISExpress",
13-
"launchBrowser": false,
13+
"launchBrowser": true,
1414
"launchUrl": "api/people",
1515
"environmentVariables": {
1616
"ASPNETCORE_ENVIRONMENT": "Development"
1717
}
1818
},
1919
"Kestrel": {
2020
"commandName": "Project",
21-
"launchBrowser": false,
21+
"launchBrowser": true,
2222
"launchUrl": "api/people",
2323
"applicationUrl": "http://localhost:14141",
2424
"environmentVariables": {

src/Examples/GettingStarted/Startup.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using Microsoft.AspNetCore.Builder;
66
using Microsoft.EntityFrameworkCore;
77
using Microsoft.Extensions.DependencyInjection;
8-
using Newtonsoft.Json;
98

109
namespace GettingStarted
1110
{
@@ -21,7 +20,7 @@ public void ConfigureServices(IServiceCollection services)
2120
options.Namespace = "api";
2221
options.UseRelativeLinks = true;
2322
options.IncludeTotalResourceCount = true;
24-
options.SerializerSettings.Formatting = Formatting.Indented;
23+
options.SerializerOptions.WriteIndented = true;
2524
});
2625
}
2726

src/Examples/JsonApiDotNetCoreExample/Controllers/NonJsonApiController.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,21 @@ public async Task<IActionResult> PostAsync()
2828
return BadRequest("Please send your name.");
2929
}
3030

31-
string result = "Hello, " + name;
31+
string result = $"Hello, {name}";
3232
return Ok(result);
3333
}
3434

3535
[HttpPut]
3636
public IActionResult Put([FromBody] string name)
3737
{
38-
string result = "Hi, " + name;
38+
string result = $"Hi, {name}";
3939
return Ok(result);
4040
}
4141

4242
[HttpPatch]
4343
public IActionResult Patch(string name)
4444
{
45-
string result = "Good day, " + name;
45+
string result = $"Good day, {name}";
4646
return Ok(result);
4747
}
4848

src/Examples/JsonApiDotNetCoreExample/Startup.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Text.Json.Serialization;
23
using JsonApiDotNetCore.Configuration;
34
using JsonApiDotNetCore.Diagnostics;
45
using JsonApiDotNetCoreExample.Data;
@@ -9,8 +10,6 @@
910
using Microsoft.Extensions.Configuration;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Logging;
12-
using Newtonsoft.Json;
13-
using Newtonsoft.Json.Converters;
1413

1514
namespace JsonApiDotNetCoreExample
1615
{
@@ -52,8 +51,8 @@ public void ConfigureServices(IServiceCollection services)
5251
options.UseRelativeLinks = true;
5352
options.ValidateModelState = true;
5453
options.IncludeTotalResourceCount = true;
55-
options.SerializerSettings.Formatting = Formatting.Indented;
56-
options.SerializerSettings.Converters.Add(new StringEnumConverter());
54+
options.SerializerOptions.WriteIndented = true;
55+
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
5756
#if DEBUG
5857
options.IncludeExceptionStackTraceInErrors = true;
5958
#endif

src/JsonApiDotNetCore/AtomicOperations/LocalIdTracker.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ private void AssertIsNotDeclared(string localId)
3232
{
3333
if (_idsTracked.ContainsKey(localId))
3434
{
35-
throw new JsonApiException(new Error(HttpStatusCode.BadRequest)
35+
throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
3636
{
3737
Title = "Another local ID with the same name is already defined at this point.",
3838
Detail = $"Another local ID with name '{localId}' is already defined at this point."
@@ -75,7 +75,7 @@ public string GetValue(string localId, string resourceType)
7575

7676
if (item.ServerId == null)
7777
{
78-
throw new JsonApiException(new Error(HttpStatusCode.BadRequest)
78+
throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
7979
{
8080
Title = "Local ID cannot be both defined and used within the same operation.",
8181
Detail = $"Local ID '{localId}' cannot be both defined and used within the same operation."
@@ -89,7 +89,7 @@ private void AssertIsDeclared(string localId)
8989
{
9090
if (!_idsTracked.ContainsKey(localId))
9191
{
92-
throw new JsonApiException(new Error(HttpStatusCode.BadRequest)
92+
throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
9393
{
9494
Title = "Server-generated value for local ID is not available at this point.",
9595
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
101101
{
102102
if (declaredType != currentType)
103103
{
104-
throw new JsonApiException(new Error(HttpStatusCode.BadRequest)
104+
throw new JsonApiException(new ErrorObject(HttpStatusCode.BadRequest)
105105
{
106106
Title = "Type mismatch in local ID usage.",
107107
Detail = $"Local ID '{localId}' belongs to resource type '{declaredType}' instead of '{currentType}'."

0 commit comments

Comments
 (0)