Skip to content

Commit d5ec214

Browse files
committed
1 parent c1fab34 commit d5ec214

15 files changed

+90
-53
lines changed

Diff for: benchmarks/Deserialization/DeserializationBenchmarkBase.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using System.ComponentModel.Design;
2-
using System.Text.Json;
32
using JetBrains.Annotations;
43
using JsonApiDotNetCore.Configuration;
54
using JsonApiDotNetCore.Middleware;
65
using JsonApiDotNetCore.Resources;
76
using JsonApiDotNetCore.Resources.Annotations;
7+
using JsonApiDotNetCore.Serialization;
88
using JsonApiDotNetCore.Serialization.JsonConverters;
99
using JsonApiDotNetCore.Serialization.Request.Adapters;
1010
using Microsoft.Extensions.Logging.Abstractions;
@@ -13,15 +13,15 @@ namespace Benchmarks.Deserialization;
1313

1414
public abstract class DeserializationBenchmarkBase
1515
{
16-
protected readonly JsonSerializerOptions SerializerReadOptions;
16+
protected readonly JsonApiSerializationContext SerializationReadContext;
1717
protected readonly DocumentAdapter DocumentAdapter;
1818

1919
protected DeserializationBenchmarkBase()
2020
{
2121
var options = new JsonApiOptions();
2222
IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<IncomingResource, int>().Build();
2323
options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));
24-
SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions;
24+
SerializationReadContext = ((IJsonApiOptions)options).SerializationReadContext;
2525

2626
var serviceContainer = new ServiceContainer();
2727
var resourceFactory = new ResourceFactory(serviceContainer);

Diff for: benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase
269269
[Benchmark]
270270
public object? DeserializeOperationsRequest()
271271
{
272-
var document = JsonSerializer.Deserialize<Document>(RequestBody, SerializerReadOptions)!;
272+
Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!;
273273
return DocumentAdapter.Convert(document);
274274
}
275275

Diff for: benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase
132132
[Benchmark]
133133
public object? DeserializeResourceRequest()
134134
{
135-
var document = JsonSerializer.Deserialize<Document>(RequestBody, SerializerReadOptions)!;
135+
Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!;
136136
return DocumentAdapter.Convert(document);
137137
}
138138

Diff for: benchmarks/Serialization/OperationsSerializationBenchmarks.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ private static IEnumerable<OperationContainer> CreateResponseOperations(IJsonApi
115115
public string SerializeOperationsResponse()
116116
{
117117
Document responseDocument = ResponseModelAdapter.Convert(_responseOperations);
118-
return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions);
118+
return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document);
119119
}
120120

121121
protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph)

Diff for: benchmarks/Serialization/ResourceSerializationBenchmarks.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private static OutgoingResource CreateResponseResource()
106106
public string SerializeResourceResponse()
107107
{
108108
Document responseDocument = ResponseModelAdapter.Convert(ResponseResource);
109-
return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions);
109+
return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document);
110110
}
111111

112112
protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph)

Diff for: benchmarks/Serialization/SerializationBenchmarkBase.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections.Immutable;
2-
using System.Text.Json;
32
using System.Text.Json.Serialization;
43
using JetBrains.Annotations;
54
using JsonApiDotNetCore.Configuration;
@@ -10,6 +9,7 @@
109
using JsonApiDotNetCore.QueryStrings;
1110
using JsonApiDotNetCore.Resources;
1211
using JsonApiDotNetCore.Resources.Annotations;
12+
using JsonApiDotNetCore.Serialization;
1313
using JsonApiDotNetCore.Serialization.Objects;
1414
using JsonApiDotNetCore.Serialization.Response;
1515
using Microsoft.AspNetCore.Http;
@@ -19,7 +19,7 @@ namespace Benchmarks.Serialization;
1919

2020
public abstract class SerializationBenchmarkBase
2121
{
22-
protected readonly JsonSerializerOptions SerializerWriteOptions;
22+
protected readonly JsonApiSerializationContext SerializationWriteContext;
2323
protected readonly IResponseModelAdapter ResponseModelAdapter;
2424
protected readonly IResourceGraph ResourceGraph;
2525

@@ -37,7 +37,7 @@ protected SerializationBenchmarkBase()
3737
};
3838

3939
ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add<OutgoingResource, int>().Build();
40-
SerializerWriteOptions = ((IJsonApiOptions)options).SerializerWriteOptions;
40+
SerializationWriteContext = ((IJsonApiOptions)options).SerializationWriteContext;
4141

4242
// ReSharper disable VirtualMemberCallInConstructor
4343
JsonApiRequest request = CreateJsonApiRequest(ResourceGraph);

Diff for: src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

+17
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
using System.Data;
22
using System.Text.Json;
3+
using JetBrains.Annotations;
34
using JsonApiDotNetCore.Resources.Annotations;
5+
using JsonApiDotNetCore.Serialization;
46
using JsonApiDotNetCore.Serialization.Objects;
57

68
namespace JsonApiDotNetCore.Configuration;
79

810
/// <summary>
911
/// Global options that configure the behavior of JsonApiDotNetCore.
1012
/// </summary>
13+
[PublicAPI]
1114
public interface IJsonApiOptions
1215
{
1316
/// <summary>
@@ -156,13 +159,27 @@ public interface IJsonApiOptions
156159
/// </example>
157160
JsonSerializerOptions SerializerOptions { get; }
158161

162+
/// <summary>
163+
/// Gets the source-generated JSON serialization context used for deserializing request bodies. This value is based on <see cref="SerializerOptions" />
164+
/// and is intended for internal use.
165+
/// </summary>
166+
JsonApiSerializationContext SerializationReadContext { get; }
167+
159168
/// <summary>
160169
/// Gets the settings used for deserializing request bodies. This value is based on <see cref="SerializerOptions" /> and is intended for internal use.
161170
/// </summary>
171+
[Obsolete("Use SerializationReadContext.Options instead.")]
162172
JsonSerializerOptions SerializerReadOptions { get; }
163173

174+
/// <summary>
175+
/// Gets the source-generated JSON serialization context used for serializing response bodies. This value is based on <see cref="SerializerOptions" />
176+
/// and is intended for internal use.
177+
/// </summary>
178+
JsonApiSerializationContext SerializationWriteContext { get; }
179+
164180
/// <summary>
165181
/// Gets the settings used for serializing response bodies. This value is based on <see cref="SerializerOptions" /> and is intended for internal use.
166182
/// </summary>
183+
[Obsolete("Use SerializationWriteContext.Options instead.")]
167184
JsonSerializerOptions SerializerWriteOptions { get; }
168185
}

Diff for: src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

+28-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Text.Json;
44
using JetBrains.Annotations;
55
using JsonApiDotNetCore.Resources.Annotations;
6+
using JsonApiDotNetCore.Serialization;
67
using JsonApiDotNetCore.Serialization.JsonConverters;
78

89
namespace JsonApiDotNetCore.Configuration;
@@ -11,14 +12,20 @@ namespace JsonApiDotNetCore.Configuration;
1112
[PublicAPI]
1213
public sealed class JsonApiOptions : IJsonApiOptions
1314
{
14-
private readonly Lazy<JsonSerializerOptions> _lazySerializerWriteOptions;
15-
private readonly Lazy<JsonSerializerOptions> _lazySerializerReadOptions;
15+
private readonly Lazy<JsonApiSerializationContext> _lazySerializerReadContext;
16+
private readonly Lazy<JsonApiSerializationContext> _lazySerializerWriteContext;
1617

1718
/// <inheritdoc />
18-
JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value;
19+
JsonApiSerializationContext IJsonApiOptions.SerializationReadContext => _lazySerializerReadContext.Value;
1920

2021
/// <inheritdoc />
21-
JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value;
22+
JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => ((IJsonApiOptions)this).SerializationReadContext.Options;
23+
24+
/// <inheritdoc />
25+
JsonApiSerializationContext IJsonApiOptions.SerializationWriteContext => _lazySerializerWriteContext.Value;
26+
27+
/// <inheritdoc />
28+
JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => ((IJsonApiOptions)this).SerializationWriteContext.Options;
2229

2330
/// <inheritdoc />
2431
public string? Namespace { get; set; }
@@ -87,7 +94,7 @@ public sealed class JsonApiOptions : IJsonApiOptions
8794
public JsonSerializerOptions SerializerOptions { get; } = new()
8895
{
8996
// These are the options common to serialization and deserialization.
90-
// At runtime, we actually use SerializerReadOptions and SerializerWriteOptions, which are customized copies of these settings,
97+
// At runtime, we actually use separate options for reading and writing (which are customized copies of these settings),
9198
// to overcome the limitation in System.Text.Json that the JsonPath is incorrect when using custom converters.
9299
// Therefore we try to avoid using custom converters has much as possible.
93100
// https://github.com/Tarmil/FSharp.SystemTextJson/issues/37
@@ -110,15 +117,24 @@ static JsonApiOptions()
110117

111118
public JsonApiOptions()
112119
{
113-
_lazySerializerReadOptions = new Lazy<JsonSerializerOptions>(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly);
120+
_lazySerializerReadContext = new Lazy<JsonApiSerializationContext>(() =>
121+
{
122+
var readOptions = new JsonSerializerOptions(SerializerOptions);
123+
return new JsonApiSerializationContext(readOptions);
124+
}, LazyThreadSafetyMode.ExecutionAndPublication);
114125

115-
_lazySerializerWriteOptions = new Lazy<JsonSerializerOptions>(() => new JsonSerializerOptions(SerializerOptions)
126+
_lazySerializerWriteContext = new Lazy<JsonApiSerializationContext>(() =>
116127
{
117-
Converters =
128+
var writeOptions = new JsonSerializerOptions(SerializerOptions)
118129
{
119-
new WriteOnlyDocumentConverter(),
120-
new WriteOnlyRelationshipObjectConverter()
121-
}
122-
}, LazyThreadSafetyMode.PublicationOnly);
130+
Converters =
131+
{
132+
new WriteOnlyDocumentConverter(),
133+
new WriteOnlyRelationshipObjectConverter()
134+
}
135+
};
136+
137+
return new JsonApiSerializationContext(writeOptions);
138+
}, LazyThreadSafetyMode.ExecutionAndPublication);
123139
}
124140
}

Diff for: src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs

+15-13
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Diagnostics;
66
using JsonApiDotNetCore.Resources.Annotations;
7+
using JsonApiDotNetCore.Serialization;
78
using JsonApiDotNetCore.Serialization.Objects;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.AspNetCore.Http.Extensions;
@@ -44,7 +45,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
4445

4546
using (CodeTimingSessionManager.Current.Measure("JSON:API middleware"))
4647
{
47-
if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions))
48+
if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializationWriteContext))
4849
{
4950
return;
5051
}
@@ -54,8 +55,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
5455

5556
if (primaryResourceType != null)
5657
{
57-
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) ||
58-
!await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions))
58+
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializationWriteContext) ||
59+
!await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializationWriteContext))
5960
{
6061
return;
6162
}
@@ -66,8 +67,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
6667
}
6768
else if (IsRouteForOperations(routeValues))
6869
{
69-
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) ||
70-
!await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions))
70+
if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializationWriteContext) ||
71+
!await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializationWriteContext))
7172
{
7273
return;
7374
}
@@ -91,11 +92,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin
9192
}
9293
}
9394

94-
private async Task<bool> ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions)
95+
private async Task<bool> ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonApiSerializationContext serializationContext)
9596
{
9697
if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch))
9798
{
98-
await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed)
99+
await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.PreconditionFailed)
99100
{
100101
Title = "Detection of mid-air edit collisions using ETags is not supported.",
101102
Source = new ErrorSource
@@ -120,13 +121,14 @@ private async Task<bool> ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso
120121
: null;
121122
}
122123

123-
private static async Task<bool> ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, JsonSerializerOptions serializerOptions)
124+
private static async Task<bool> ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext,
125+
JsonApiSerializationContext serializationContext)
124126
{
125127
string? contentType = httpContext.Request.ContentType;
126128

127129
if (contentType != null && contentType != allowedContentType)
128130
{
129-
await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.UnsupportedMediaType)
131+
await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.UnsupportedMediaType)
130132
{
131133
Title = "The specified Content-Type header value is not supported.",
132134
Detail = $"Please specify '{allowedContentType}' instead of '{contentType}' for the Content-Type header value.",
@@ -143,7 +145,7 @@ private static async Task<bool> ValidateContentTypeHeaderAsync(string allowedCon
143145
}
144146

145147
private static async Task<bool> ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext,
146-
JsonSerializerOptions serializerOptions)
148+
JsonApiSerializationContext serializationContext)
147149
{
148150
string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept");
149151

@@ -176,7 +178,7 @@ private static async Task<bool> ValidateAcceptHeaderAsync(MediaTypeHeaderValue a
176178

177179
if (!seenCompatibleMediaType)
178180
{
179-
await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.NotAcceptable)
181+
await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.NotAcceptable)
180182
{
181183
Title = "The specified Accept header value does not contain any supported media types.",
182184
Detail = $"Please include '{allowedMediaTypeValue}' in the Accept header values.",
@@ -192,7 +194,7 @@ private static async Task<bool> ValidateAcceptHeaderAsync(MediaTypeHeaderValue a
192194
return true;
193195
}
194196

195-
private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error)
197+
private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiSerializationContext serializationContext, ErrorObject error)
196198
{
197199
httpResponse.ContentType = HeaderConstants.MediaType;
198200
httpResponse.StatusCode = (int)error.StatusCode;
@@ -202,7 +204,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri
202204
Errors = error.AsList()
203205
};
204206

205-
await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions);
207+
await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializationContext.Document);
206208
await httpResponse.Body.FlushAsync();
207209
}
208210

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Text.Json.Serialization;
2+
using JsonApiDotNetCore.Serialization.Objects;
3+
4+
namespace JsonApiDotNetCore.Serialization;
5+
6+
/// <summary>
7+
/// Provides compile-time metadata about the set of JSON:API types used in JSON serialization of request/response bodies.
8+
/// </summary>
9+
[JsonSerializable(typeof(Document))]
10+
public sealed partial class JsonApiSerializationContext : JsonSerializerContext
11+
{
12+
}

Diff for: src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs

+1-13
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,12 @@ public abstract class JsonObjectConverter<TObject> : JsonConverter<TObject>
77
{
88
protected static TValue? ReadSubTree<TValue>(ref Utf8JsonReader reader, JsonSerializerOptions options)
99
{
10-
if (typeof(TValue) != typeof(object) && options.GetConverter(typeof(TValue)) is JsonConverter<TValue> converter)
11-
{
12-
return converter.Read(ref reader, typeof(TValue), options);
13-
}
14-
1510
return JsonSerializer.Deserialize<TValue>(ref reader, options);
1611
}
1712

1813
protected static void WriteSubTree<TValue>(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options)
1914
{
20-
if (typeof(TValue) != typeof(object) && options.GetConverter(typeof(TValue)) is JsonConverter<TValue> converter)
21-
{
22-
converter.Write(writer, value, options);
23-
}
24-
else
25-
{
26-
JsonSerializer.Serialize(writer, value, options);
27-
}
15+
JsonSerializer.Serialize(writer, value, options);
2816
}
2917

3018
protected static JsonException GetEndOfStreamError()

Diff for: src/JsonApiDotNetCore/Serialization/Objects/Document.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System.Text.Json.Serialization;
2+
using JetBrains.Annotations;
23

34
namespace JsonApiDotNetCore.Serialization.Objects;
45

56
/// <summary>
67
/// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure.
78
/// </summary>
9+
[PublicAPI]
810
public sealed class Document
911
{
1012
[JsonPropertyName("jsonapi")]

Diff for: src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ private Document DeserializeDocument(string requestBody)
8080
using IDisposable _ =
8181
CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages);
8282

83-
var document = JsonSerializer.Deserialize<Document>(requestBody, _options.SerializerReadOptions);
83+
Document? document = JsonSerializer.Deserialize(requestBody, _options.SerializationReadContext.Document);
8484

8585
AssertHasDocument(document, requestBody);
8686

Diff for: src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ private string SerializeDocument(Document document)
125125
{
126126
using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages);
127127

128-
return JsonSerializer.Serialize(document, _options.SerializerWriteOptions);
128+
return JsonSerializer.Serialize(document, _options.SerializationWriteContext.Document);
129129
}
130130

131131
private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent)

0 commit comments

Comments
 (0)