From 826f29089125c74f02cf01ed7bf447fdb230d7bc Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 17 Jul 2022 22:18:33 +0200 Subject: [PATCH 1/5] Minor fixes --- src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs | 9 +++++---- src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs | 4 ++-- .../Queries/Internal/Parsing/IncludeParser.cs | 10 +++++----- .../JsonConverters/SingleOrManyDataConverterFactory.cs | 4 ++-- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index eda6374acf..2e7cc54282 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -11,8 +11,8 @@ namespace JsonApiDotNetCore.Configuration; [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - private Lazy _lazySerializerWriteOptions; - private Lazy _lazySerializerReadOptions; + private readonly Lazy _lazySerializerWriteOptions; + private readonly Lazy _lazySerializerReadOptions; /// JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; @@ -110,7 +110,8 @@ static JsonApiOptions() public JsonApiOptions() { - _lazySerializerReadOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly); + _lazySerializerReadOptions = + new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.ExecutionAndPublication); _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) { @@ -119,6 +120,6 @@ public JsonApiOptions() new WriteOnlyDocumentConverter(), new WriteOnlyRelationshipObjectConverter() } - }, LazyThreadSafetyMode.PublicationOnly); + }, LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 94507750da..2e15e6ae9a 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -72,7 +72,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin return; } - SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request); + SetupOperationsRequest((JsonApiRequest)request); httpContext.RegisterJsonApiRequest(); } @@ -280,7 +280,7 @@ private static bool IsRouteForOperations(RouteValueDictionary routeValues) return actionName == "PostOperations"; } - private static void SetupOperationsRequest(JsonApiRequest request, IJsonApiOptions options, HttpRequest httpRequest) + private static void SetupOperationsRequest(JsonApiRequest request) { request.IsReadOnly = false; request.Kind = EndpointKind.AtomicOperations; diff --git a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs index a453921989..14d2f1ec15 100644 --- a/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Internal/Parsing/IncludeParser.cs @@ -202,7 +202,7 @@ public string Path var pathBuilder = new StringBuilder(); IncludeTreeNode? parent = this; - while (parent is { Relationship: not HiddenRootRelationship }) + while (parent is { Relationship: not HiddenRootRelationshipAttribute }) { pathBuilder.Insert(0, pathBuilder.Length > 0 ? $"{parent.Relationship.PublicName}." : parent.Relationship.PublicName); parent = parent._parent; @@ -220,7 +220,7 @@ private IncludeTreeNode(RelationshipAttribute relationship, IncludeTreeNode? par public static IncludeTreeNode CreateRoot(ResourceType resourceType) { - var relationship = new HiddenRootRelationship(resourceType); + var relationship = new HiddenRootRelationshipAttribute(resourceType); return new IncludeTreeNode(relationship, null); } @@ -242,7 +242,7 @@ public IncludeExpression ToExpression() { IncludeElementExpression element = ToElementExpression(); - if (element.Relationship is HiddenRootRelationship) + if (element.Relationship is HiddenRootRelationshipAttribute) { return new IncludeExpression(element.Children); } @@ -262,9 +262,9 @@ public override string ToString() return include.ToFullString(); } - private sealed class HiddenRootRelationship : RelationshipAttribute + private sealed class HiddenRootRelationshipAttribute : RelationshipAttribute { - public HiddenRootRelationship(ResourceType rightType) + public HiddenRootRelationshipAttribute(ResourceType rightType) { ArgumentGuard.NotNull(rightType, nameof(rightType)); diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs index 25e497c2c1..3952ca93d8 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs @@ -28,7 +28,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer private sealed class SingleOrManyDataConverter : JsonObjectConverter> where T : class, IResourceIdentity, new() { - public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions serializerOptions) + public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var objects = new List(); bool isManyData = false; @@ -54,7 +54,7 @@ public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToC } case JsonTokenType.StartObject: { - var resourceObject = ReadSubTree(ref reader, serializerOptions); + var resourceObject = ReadSubTree(ref reader, options); objects.Add(resourceObject); break; } From 96340092d6d388ea860ff21e9bbf9fdeaf11c817 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 17 Jul 2022 22:51:49 +0200 Subject: [PATCH 2/5] Reduce duplication in JSON:API type hierarchy by using JsonPropertyOrder (added in .NET 6). Also makes the model a bit more correct, as "ref" elements (used in atomic:operations) cannot occur in "data". --- .../SingleOrManyDataConverterFactory.cs | 2 +- .../Serialization/Objects/AtomicReference.cs | 14 +--------- .../Objects/IResourceIdentity.cs | 8 ------ .../Objects/ResourceIdentifierObject.cs | 15 ++--------- .../Serialization/Objects/ResourceIdentity.cs | 26 +++++++++++++++++++ .../Serialization/Objects/ResourceObject.cs | 21 +++------------ .../Serialization/Objects/SingleOrManyData.cs | 2 +- .../Request/Adapters/BaseAdapter.cs | 6 ++--- .../Adapters/ResourceIdentityAdapter.cs | 20 +++++++------- .../Adapters/ResourceIdentityRequirements.cs | 2 +- 10 files changed, 49 insertions(+), 67 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs create mode 100644 src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs index 3952ca93d8..b842cace0e 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/SingleOrManyDataConverterFactory.cs @@ -26,7 +26,7 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer } private sealed class SingleOrManyDataConverter : JsonObjectConverter> - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { public override SingleOrManyData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs index 01693d1db6..fcc56298c1 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/AtomicReference.cs @@ -7,20 +7,8 @@ namespace JsonApiDotNetCore.Serialization.Objects; /// See "ref" in https://jsonapi.org/ext/atomic/#operation-objects. /// [PublicAPI] -public sealed class AtomicReference : IResourceIdentity +public sealed class AtomicReference : ResourceIdentity { - [JsonPropertyName("type")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string? Type { get; set; } - - [JsonPropertyName("id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Id { get; set; } - - [JsonPropertyName("lid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Lid { get; set; } - [JsonPropertyName("relationship")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Relationship { get; set; } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs b/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs deleted file mode 100644 index c4b57f535f..0000000000 --- a/src/JsonApiDotNetCore/Serialization/Objects/IResourceIdentity.cs +++ /dev/null @@ -1,8 +0,0 @@ -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/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs index a1b8271cf7..20c30909ed 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentifierObject.cs @@ -7,21 +7,10 @@ namespace JsonApiDotNetCore.Serialization.Objects; /// See https://jsonapi.org/format/1.1/#document-resource-identifier-objects. /// [PublicAPI] -public sealed class ResourceIdentifierObject : IResourceIdentity +public class ResourceIdentifierObject : ResourceIdentity { - [JsonPropertyName("type")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string? Type { get; set; } - - [JsonPropertyName("id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Id { get; set; } - - [JsonPropertyName("lid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Lid { get; set; } - [JsonPropertyName("meta")] + [JsonPropertyOrder(100)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? Meta { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs new file mode 100644 index 0000000000..41a3d951e6 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceIdentity.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.Serialization.Objects; + +/// +/// Shared identity information for various JSON:API objects. +/// +[PublicAPI] +public abstract class ResourceIdentity +{ + [JsonPropertyName("type")] + [JsonPropertyOrder(-3)] + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public string? Type { get; set; } + + [JsonPropertyName("id")] + [JsonPropertyOrder(-2)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Id { get; set; } + + [JsonPropertyName("lid")] + [JsonPropertyOrder(-1)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? Lid { get; set; } +} diff --git a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs index 43b3b9616a..ed38a40f9a 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/ResourceObject.cs @@ -7,33 +7,20 @@ namespace JsonApiDotNetCore.Serialization.Objects; /// See https://jsonapi.org/format/1.1/#document-resource-objects. /// [PublicAPI] -public sealed class ResourceObject : IResourceIdentity +public sealed class ResourceObject : ResourceIdentifierObject { - [JsonPropertyName("type")] - [JsonIgnore(Condition = JsonIgnoreCondition.Never)] - public string? Type { get; set; } - - [JsonPropertyName("id")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Id { get; set; } - - [JsonPropertyName("lid")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Lid { get; set; } - [JsonPropertyName("attributes")] + [JsonPropertyOrder(1)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? Attributes { get; set; } [JsonPropertyName("relationships")] + [JsonPropertyOrder(2)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? Relationships { get; set; } [JsonPropertyName("links")] + [JsonPropertyOrder(3)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ResourceLinks? Links { get; set; } - - [JsonPropertyName("meta")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public IDictionary? Meta { get; set; } } diff --git a/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs index 1d2f99e126..1126f84f26 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/SingleOrManyData.cs @@ -13,7 +13,7 @@ namespace JsonApiDotNetCore.Serialization.Objects; public readonly struct SingleOrManyData // The "new()" constraint exists for parity with SingleOrManyDataConverterFactory, which creates empty instances // to ensure ManyValue never contains null items. - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { public object? Value => ManyValue != null ? ManyValue : SingleValue; diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs index 64e2f6d53b..fb1111bea1 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/BaseAdapter.cs @@ -11,7 +11,7 @@ public abstract class BaseAdapter { [AssertionMethod] protected static void AssertHasData(SingleOrManyData data, RequestAdapterState state) - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { if (!data.IsAssigned) { @@ -21,7 +21,7 @@ protected static void AssertHasData(SingleOrManyData data, RequestAdapterS [AssertionMethod] protected static void AssertDataHasSingleValue(SingleOrManyData data, bool allowNull, RequestAdapterState state) - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { if (data.SingleValue == null) { @@ -44,7 +44,7 @@ protected static void AssertDataHasSingleValue(SingleOrManyData data, bool [AssertionMethod] protected static void AssertDataHasManyValue(SingleOrManyData data, RequestAdapterState state) - where T : class, IResourceIdentity, new() + where T : ResourceIdentifierObject, new() { if (data.ManyValue == null) { diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs index d163eb56d1..61c6cc1857 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs @@ -25,7 +25,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory _resourceFactory = resourceFactory; } - protected (IIdentifiable resource, ResourceType resourceType) ConvertResourceIdentity(IResourceIdentity identity, ResourceIdentityRequirements requirements, + protected (IIdentifiable resource, ResourceType resourceType) ConvertResourceIdentity(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { ArgumentGuard.NotNull(identity, nameof(identity)); @@ -38,7 +38,7 @@ protected ResourceIdentityAdapter(IResourceGraph resourceGraph, IResourceFactory return (resource, resourceType); } - private ResourceType ResolveType(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + private ResourceType ResolveType(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { AssertHasType(identity.Type, state); @@ -93,7 +93,7 @@ private static void AssertIsCompatibleResourceType(ResourceType actual, Resource } } - private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceClrType, RequestAdapterState state) + private IIdentifiable CreateResource(ResourceIdentity identity, ResourceIdentityRequirements requirements, Type resourceClrType, RequestAdapterState state) { if (state.Request.Kind != EndpointKind.AtomicOperations) { @@ -120,7 +120,7 @@ private IIdentifiable CreateResource(IResourceIdentity identity, ResourceIdentit return resource; } - private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterState state) + private static void AssertHasNoLid(ResourceIdentity identity, RequestAdapterState state) { if (identity.Lid != null) { @@ -129,7 +129,7 @@ private static void AssertHasNoLid(IResourceIdentity identity, RequestAdapterSta } } - private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapterState state) + private static void AssertNoIdWithLid(ResourceIdentity identity, RequestAdapterState state) { if (identity.Id != null && identity.Lid != null) { @@ -137,7 +137,7 @@ private static void AssertNoIdWithLid(IResourceIdentity identity, RequestAdapter } } - private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) + private static void AssertHasIdOrLid(ResourceIdentity identity, ResourceIdentityRequirements requirements, RequestAdapterState state) { string? message = null; @@ -160,7 +160,7 @@ private static void AssertHasIdOrLid(IResourceIdentity identity, ResourceIdentit } } - private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterState state) + private static void AssertHasNoId(ResourceIdentity identity, RequestAdapterState state) { if (identity.Id != null) { @@ -169,7 +169,7 @@ private static void AssertHasNoId(IResourceIdentity identity, RequestAdapterStat } } - private static void AssertSameIdValue(IResourceIdentity identity, string? expected, RequestAdapterState state) + private static void AssertSameIdValue(ResourceIdentity identity, string? expected, RequestAdapterState state) { if (expected != null && identity.Id != expected) { @@ -180,7 +180,7 @@ private static void AssertSameIdValue(IResourceIdentity identity, string? expect } } - private static void AssertSameLidValue(IResourceIdentity identity, string? expected, RequestAdapterState state) + private static void AssertSameLidValue(ResourceIdentity identity, string? expected, RequestAdapterState state) { if (expected != null && identity.Lid != expected) { @@ -191,7 +191,7 @@ private static void AssertSameLidValue(IResourceIdentity identity, string? expec } } - private void AssignStringId(IResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) + private void AssignStringId(ResourceIdentity identity, IIdentifiable resource, RequestAdapterState state) { if (identity.Id != null) { diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs index 11db5e8ee3..d5498397bf 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityRequirements.cs @@ -5,7 +5,7 @@ namespace JsonApiDotNetCore.Serialization.Request.Adapters; /// -/// Defines requirements to validate an instance against. +/// Defines requirements to validate a instance against. /// [PublicAPI] public sealed class ResourceIdentityRequirements From 792ab16e44eb090465bf2e28ef9d016987aab05d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 21 Aug 2022 13:56:20 +0200 Subject: [PATCH 3/5] Tweak benchmarks --- benchmarks/Benchmarks.csproj | 4 +- .../OperationsDeserializationBenchmarks.cs | 1 + .../ResourceDeserializationBenchmarks.cs | 1 + .../QueryStringParserBenchmarks.cs | 31 +--- .../OperationsSerializationBenchmarks.cs | 1 + .../ResourceSerializationBenchmarks.cs | 1 + .../SerializationBenchmarkBase.cs | 147 +----------------- benchmarks/Tools/FakeLinkBuilder.cs | 39 +++++ .../Tools/FakeRequestQueryStringAccessor.cs | 18 +++ .../Tools/NeverResourceDefinitionAccessor.cs | 103 ++++++++++++ benchmarks/Tools/NoMetaBuilder.cs | 18 +++ 11 files changed, 191 insertions(+), 173 deletions(-) create mode 100644 benchmarks/Tools/FakeLinkBuilder.cs create mode 100644 benchmarks/Tools/FakeRequestQueryStringAccessor.cs create mode 100644 benchmarks/Tools/NeverResourceDefinitionAccessor.cs create mode 100644 benchmarks/Tools/NoMetaBuilder.cs diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj index 4bde435c15..f461a4831b 100644 --- a/benchmarks/Benchmarks.csproj +++ b/benchmarks/Benchmarks.csproj @@ -1,7 +1,8 @@ - + Exe $(TargetFrameworkName) + true @@ -10,6 +11,5 @@ - diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs index d28684e27b..99adce73cb 100644 --- a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -7,6 +7,7 @@ namespace Benchmarks.Deserialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase { diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index 23a6205bf5..e503a329bb 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -7,6 +7,7 @@ namespace Benchmarks.Deserialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase { diff --git a/benchmarks/QueryString/QueryStringParserBenchmarks.cs b/benchmarks/QueryString/QueryStringParserBenchmarks.cs index efa4f12659..4218c2e3dc 100644 --- a/benchmarks/QueryString/QueryStringParserBenchmarks.cs +++ b/benchmarks/QueryString/QueryStringParserBenchmarks.cs @@ -1,13 +1,12 @@ using System.ComponentModel.Design; using BenchmarkDotNet.Attributes; +using Benchmarks.Tools; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.QueryStrings.Internal; using JsonApiDotNetCore.Resources; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging.Abstractions; namespace Benchmarks.QueryString; @@ -71,31 +70,9 @@ public void DescendingSort() [Benchmark] public void ComplexQuery() { - Run(100, () => - { - const string queryString = - "?filter[alt-attr-name]=abc,eq:abc&sort=-alt-attr-name&include=child&page[size]=1&fields[alt-resource-name]=alt-attr-name"; - - _queryStringAccessor.SetQueryString(queryString); - _queryStringReader.ReadAll(null); - }); - } - - private void Run(int iterations, Action action) - { - for (int index = 0; index < iterations; index++) - { - action(); - } - } - - private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor - { - public IQueryCollection Query { get; private set; } = new QueryCollection(); + const string queryString = "?filter[alt-attr-name]=abc,eq:abc&sort=-alt-attr-name&include=child&page[size]=1&fields[alt-resource-name]=alt-attr-name"; - public void SetQueryString(string queryString) - { - Query = new QueryCollection(QueryHelpers.ParseQuery(queryString)); - } + _queryStringAccessor.SetQueryString(queryString); + _queryStringReader.ReadAll(null); } } diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 7076ca5cb8..471c9604c7 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -9,6 +9,7 @@ namespace Benchmarks.Serialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class OperationsSerializationBenchmarks : SerializationBenchmarkBase { diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index 12f5c2e788..a985bd5936 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -12,6 +12,7 @@ namespace Benchmarks.Serialization; [MarkdownExporter] +[MemoryDiagnoser] // ReSharper disable once ClassCanBeSealed.Global public class ResourceSerializationBenchmarks : SerializationBenchmarkBase { diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index e1bcb10843..d9cfefd0b6 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -1,18 +1,14 @@ -using System.Collections.Immutable; using System.Text.Json; using System.Text.Json.Serialization; +using Benchmarks.Tools; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Queries; -using JsonApiDotNetCore.Queries.Expressions; using JsonApiDotNetCore.Queries.Internal; -using JsonApiDotNetCore.QueryStrings; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCore.Serialization.Response; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Abstractions; namespace Benchmarks.Serialization; @@ -45,9 +41,9 @@ protected SerializationBenchmarkBase() // ReSharper restore VirtualMemberCallInConstructor var linkBuilder = new FakeLinkBuilder(); - var metaBuilder = new FakeMetaBuilder(); + var metaBuilder = new NoMetaBuilder(); IQueryConstraintProvider[] constraintProviders = Array.Empty(); - var resourceDefinitionAccessor = new FakeResourceDefinitionAccessor(); + var resourceDefinitionAccessor = new NeverResourceDefinitionAccessor(); var sparseFieldSetCache = new SparseFieldSetCache(constraintProviders, resourceDefinitionAccessor); var requestQueryStringAccessor = new FakeRequestQueryStringAccessor(); @@ -122,141 +118,4 @@ public sealed class OutgoingResource : Identifiable [HasMany] public ISet Multi5 { get; set; } = null!; } - - private sealed class FakeResourceDefinitionAccessor : IResourceDefinitionAccessor - { - public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) - { - return existingIncludes; - } - - public FilterExpression? OnApplyFilter(ResourceType resourceType, FilterExpression? existingFilter) - { - return existingFilter; - } - - public SortExpression? OnApplySort(ResourceType resourceType, SortExpression? existingSort) - { - return existingSort; - } - - public PaginationExpression? OnApplyPagination(ResourceType resourceType, PaginationExpression? existingPagination) - { - return existingPagination; - } - - public SparseFieldSetExpression? OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression? existingSparseFieldSet) - { - return existingSparseFieldSet; - } - - public object? GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) - { - return null; - } - - public IDictionary? GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) - { - return null; - } - - public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, - IIdentifiable? rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.FromResult(rightResourceId); - } - - public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, - CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) - where TResource : class, IIdentifiable - { - return Task.CompletedTask; - } - - public void OnDeserialize(IIdentifiable resource) - { - } - - public void OnSerialize(IIdentifiable resource) - { - } - } - - private sealed class FakeLinkBuilder : ILinkBuilder - { - public TopLevelLinks GetTopLevelLinks() - { - return new TopLevelLinks - { - Self = "TopLevel:Self" - }; - } - - public ResourceLinks GetResourceLinks(ResourceType resourceType, IIdentifiable resource) - { - return new ResourceLinks - { - Self = "Resource:Self" - }; - } - - public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) - { - return new RelationshipLinks - { - Self = "Relationship:Self", - Related = "Relationship:Related" - }; - } - } - - private sealed class FakeMetaBuilder : IMetaBuilder - { - public void Add(IDictionary values) - { - } - - public IDictionary? Build() - { - return null; - } - } - - private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor - { - public IQueryCollection Query { get; } = new QueryCollection(0); - } } diff --git a/benchmarks/Tools/FakeLinkBuilder.cs b/benchmarks/Tools/FakeLinkBuilder.cs new file mode 100644 index 0000000000..3468237507 --- /dev/null +++ b/benchmarks/Tools/FakeLinkBuilder.cs @@ -0,0 +1,39 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization.Objects; +using JsonApiDotNetCore.Serialization.Response; +using Microsoft.AspNetCore.Http; + +namespace Benchmarks.Tools; + +/// +/// Renders hard-coded fake links, without depending on . +/// +internal sealed class FakeLinkBuilder : ILinkBuilder +{ + public TopLevelLinks GetTopLevelLinks() + { + return new TopLevelLinks + { + Self = "TopLevel:Self" + }; + } + + public ResourceLinks GetResourceLinks(ResourceType resourceType, IIdentifiable resource) + { + return new ResourceLinks + { + Self = "Resource:Self" + }; + } + + public RelationshipLinks GetRelationshipLinks(RelationshipAttribute relationship, IIdentifiable leftResource) + { + return new RelationshipLinks + { + Self = "Relationship:Self", + Related = "Relationship:Related" + }; + } +} diff --git a/benchmarks/Tools/FakeRequestQueryStringAccessor.cs b/benchmarks/Tools/FakeRequestQueryStringAccessor.cs new file mode 100644 index 0000000000..8b2b5540a1 --- /dev/null +++ b/benchmarks/Tools/FakeRequestQueryStringAccessor.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.QueryStrings; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.WebUtilities; + +namespace Benchmarks.Tools; + +/// +/// Enables to inject a query string, instead of obtaining it from . +/// +internal sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor +{ + public IQueryCollection Query { get; private set; } = new QueryCollection(); + + public void SetQueryString(string queryString) + { + Query = new QueryCollection(QueryHelpers.ParseQuery(queryString)); + } +} diff --git a/benchmarks/Tools/NeverResourceDefinitionAccessor.cs b/benchmarks/Tools/NeverResourceDefinitionAccessor.cs new file mode 100644 index 0000000000..6e93519dae --- /dev/null +++ b/benchmarks/Tools/NeverResourceDefinitionAccessor.cs @@ -0,0 +1,103 @@ +using System.Collections.Immutable; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Queries.Expressions; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace Benchmarks.Tools; + +/// +/// Never calls into instances. +/// +internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor +{ + public IImmutableSet OnApplyIncludes(ResourceType resourceType, IImmutableSet existingIncludes) + { + return existingIncludes; + } + + public FilterExpression? OnApplyFilter(ResourceType resourceType, FilterExpression? existingFilter) + { + return existingFilter; + } + + public SortExpression? OnApplySort(ResourceType resourceType, SortExpression? existingSort) + { + return existingSort; + } + + public PaginationExpression? OnApplyPagination(ResourceType resourceType, PaginationExpression? existingPagination) + { + return existingPagination; + } + + public SparseFieldSetExpression? OnApplySparseFieldSet(ResourceType resourceType, SparseFieldSetExpression? existingSparseFieldSet) + { + return existingSparseFieldSet; + } + + public object? GetQueryableHandlerForQueryStringParameter(Type resourceClrType, string parameterName) + { + return null; + } + + public IDictionary? GetMeta(ResourceType resourceType, IIdentifiable resourceInstance) + { + return null; + } + + public Task OnPrepareWriteAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnSetToOneRelationshipAsync(TResource leftResource, HasOneAttribute hasOneRelationship, + IIdentifiable? rightResourceId, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.FromResult(rightResourceId); + } + + public Task OnSetToManyRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnAddToRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnRemoveFromRelationshipAsync(TResource leftResource, HasManyAttribute hasManyRelationship, ISet rightResourceIds, + CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnWritingAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public Task OnWriteSucceededAsync(TResource resource, WriteOperationKind writeOperation, CancellationToken cancellationToken) + where TResource : class, IIdentifiable + { + return Task.CompletedTask; + } + + public void OnDeserialize(IIdentifiable resource) + { + } + + public void OnSerialize(IIdentifiable resource) + { + } +} diff --git a/benchmarks/Tools/NoMetaBuilder.cs b/benchmarks/Tools/NoMetaBuilder.cs new file mode 100644 index 0000000000..db3ed7857e --- /dev/null +++ b/benchmarks/Tools/NoMetaBuilder.cs @@ -0,0 +1,18 @@ +using JsonApiDotNetCore.Serialization.Response; + +namespace Benchmarks.Tools; + +/// +/// Doesn't produce any top-level meta. +/// +internal sealed class NoMetaBuilder : IMetaBuilder +{ + public void Add(IDictionary values) + { + } + + public IDictionary? Build() + { + return null; + } +} From a00ab50528ed6ba3db84d536287450d29b49a92d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Mon, 18 Jul 2022 02:01:56 +0200 Subject: [PATCH 4/5] Use System.Text.Json source generator (see https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator) --- .../DeserializationBenchmarkBase.cs | 6 ++-- .../OperationsDeserializationBenchmarks.cs | 2 +- .../ResourceDeserializationBenchmarks.cs | 2 +- .../OperationsSerializationBenchmarks.cs | 2 +- .../ResourceSerializationBenchmarks.cs | 2 +- .../SerializationBenchmarkBase.cs | 6 ++-- .../Configuration/IJsonApiOptions.cs | 17 +++++++++++ .../Configuration/JsonApiOptions.cs | 23 +++++++++------ .../Middleware/JsonApiMiddleware.cs | 28 ++++++++++--------- .../JsonApiSerializationContext.cs | 17 +++++++++++ .../JsonConverters/JsonObjectConverter.cs | 14 +--------- .../Serialization/Objects/Document.cs | 2 ++ .../Serialization/Request/JsonApiReader.cs | 2 +- .../Serialization/Response/JsonApiWriter.cs | 2 +- .../Response/ResponseModelAdapterTests.cs | 6 ++-- 15 files changed, 82 insertions(+), 49 deletions(-) create mode 100644 src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index bbf746d1a8..80a9753597 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -1,10 +1,10 @@ using System.ComponentModel.Design; -using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.Extensions.Logging.Abstractions; @@ -13,7 +13,7 @@ namespace Benchmarks.Deserialization; public abstract class DeserializationBenchmarkBase { - protected readonly JsonSerializerOptions SerializerReadOptions; + protected readonly JsonApiSerializationContext SerializationReadContext; protected readonly DocumentAdapter DocumentAdapter; protected DeserializationBenchmarkBase() @@ -21,7 +21,7 @@ protected DeserializationBenchmarkBase() var options = new JsonApiOptions(); IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); - SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions; + SerializationReadContext = ((IJsonApiOptions)options).SerializationReadContext; var serviceContainer = new ServiceContainer(); var resourceFactory = new ResourceFactory(serviceContainer); diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs index 99adce73cb..efe0ae568f 100644 --- a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -270,7 +270,7 @@ public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeOperationsRequest() { - var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; + Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index e503a329bb..3d2cdd35af 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -133,7 +133,7 @@ public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeResourceRequest() { - var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; + Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 471c9604c7..2be9da5da6 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -116,7 +116,7 @@ private static IEnumerable CreateResponseOperations(IJsonApi public string SerializeOperationsResponse() { Document responseDocument = ResponseModelAdapter.Convert(_responseOperations); - return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); + return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index a985bd5936..f896846ee2 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -107,7 +107,7 @@ private static OutgoingResource CreateResponseResource() public string SerializeResourceResponse() { Document responseDocument = ResponseModelAdapter.Convert(ResponseResource); - return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); + return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index d9cfefd0b6..be1a711ad9 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using System.Text.Json.Serialization; using Benchmarks.Tools; using JetBrains.Annotations; @@ -8,6 +7,7 @@ using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Response; using Microsoft.Extensions.Logging.Abstractions; @@ -15,7 +15,7 @@ namespace Benchmarks.Serialization; public abstract class SerializationBenchmarkBase { - protected readonly JsonSerializerOptions SerializerWriteOptions; + protected readonly JsonApiSerializationContext SerializationWriteContext; protected readonly IResponseModelAdapter ResponseModelAdapter; protected readonly IResourceGraph ResourceGraph; @@ -33,7 +33,7 @@ protected SerializationBenchmarkBase() }; ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - SerializerWriteOptions = ((IJsonApiOptions)options).SerializerWriteOptions; + SerializationWriteContext = ((IJsonApiOptions)options).SerializationWriteContext; // ReSharper disable VirtualMemberCallInConstructor JsonApiRequest request = CreateJsonApiRequest(ResourceGraph); diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 597d22294d..477500d79b 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,6 +1,8 @@ using System.Data; using System.Text.Json; +using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Configuration; @@ -8,6 +10,7 @@ namespace JsonApiDotNetCore.Configuration; /// /// Global options that configure the behavior of JsonApiDotNetCore. /// +[PublicAPI] public interface IJsonApiOptions { /// @@ -156,13 +159,27 @@ public interface IJsonApiOptions /// JsonSerializerOptions SerializerOptions { get; } + /// + /// Gets the source-generated JSON serialization context used for deserializing request bodies. This value is based on + /// and is intended for internal use. + /// + JsonApiSerializationContext SerializationReadContext { get; } + /// /// Gets the settings used for deserializing request bodies. This value is based on and is intended for internal use. /// + [Obsolete("Use SerializationReadContext.Options instead.")] JsonSerializerOptions SerializerReadOptions { get; } + /// + /// Gets the source-generated JSON serialization context used for serializing response bodies. This value is based on + /// and is intended for internal use. + /// + JsonApiSerializationContext SerializationWriteContext { get; } + /// /// Gets the settings used for serializing response bodies. This value is based on and is intended for internal use. /// + [Obsolete("Use SerializationWriteContext.Options instead.")] JsonSerializerOptions SerializerWriteOptions { get; } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index 2e7cc54282..bb167e4db2 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -3,6 +3,7 @@ using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; +using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; namespace JsonApiDotNetCore.Configuration; @@ -11,14 +12,20 @@ namespace JsonApiDotNetCore.Configuration; [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - private readonly Lazy _lazySerializerWriteOptions; - private readonly Lazy _lazySerializerReadOptions; + private readonly Lazy _lazySerializerReadContext; + private readonly Lazy _lazySerializerWriteContext; /// - JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; + JsonApiSerializationContext IJsonApiOptions.SerializationReadContext => _lazySerializerReadContext.Value; /// - JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value; + JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => ((IJsonApiOptions)this).SerializationReadContext.Options; + + /// + JsonApiSerializationContext IJsonApiOptions.SerializationWriteContext => _lazySerializerWriteContext.Value; + + /// + JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => ((IJsonApiOptions)this).SerializationWriteContext.Options; /// public string? Namespace { get; set; } @@ -110,16 +117,16 @@ static JsonApiOptions() public JsonApiOptions() { - _lazySerializerReadOptions = - new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.ExecutionAndPublication); + _lazySerializerReadContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions)), + LazyThreadSafetyMode.ExecutionAndPublication); - _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) + _lazySerializerWriteContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions) { Converters = { new WriteOnlyDocumentConverter(), new WriteOnlyRelationshipObjectConverter() } - }, LazyThreadSafetyMode.ExecutionAndPublication); + }), LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 2e15e6ae9a..6ea3853e92 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -4,6 +4,7 @@ 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; @@ -44,7 +45,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializationWriteContext)) { return; } @@ -54,8 +55,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin if (primaryResourceType != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializationWriteContext) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializationWriteContext)) { return; } @@ -66,8 +67,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } else if (IsRouteForOperations(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializationWriteContext) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializationWriteContext)) { return; } @@ -91,11 +92,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } } - private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions) + private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonApiSerializationContext serializationContext) { if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { - await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed) + await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.PreconditionFailed) { Title = "Detection of mid-air edit collisions using ETags is not supported.", Source = new ErrorSource @@ -120,13 +121,14 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso : null; } - private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, JsonSerializerOptions serializerOptions) + private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, + JsonApiSerializationContext serializationContext) { string? contentType = httpContext.Request.ContentType; if (contentType != null && contentType != allowedContentType) { - await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.UnsupportedMediaType) + await FlushResponseAsync(httpContext.Response, serializationContext, 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.", @@ -143,7 +145,7 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon } private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, - JsonSerializerOptions serializerOptions) + JsonApiSerializationContext serializationContext) { string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept"); @@ -176,7 +178,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a if (!seenCompatibleMediaType) { - await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.NotAcceptable) + await FlushResponseAsync(httpContext.Response, serializationContext, 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.", @@ -192,7 +194,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a return true; } - private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error) + private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiSerializationContext serializationContext, ErrorObject error) { httpResponse.ContentType = HeaderConstants.MediaType; httpResponse.StatusCode = (int)error.StatusCode; @@ -202,7 +204,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSeri Errors = error.AsList() }; - await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions); + await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializationContext.Document); await httpResponse.Body.FlushAsync(); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs new file mode 100644 index 0000000000..38180d9369 --- /dev/null +++ b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using JsonApiDotNetCore.Serialization.Objects; + +namespace JsonApiDotNetCore.Serialization; + +// Workaround for https://youtrack.jetbrains.com/issue/RSRP-487028 +partial class JsonApiSerializationContext +{ +} + +/// +/// Provides compile-time metadata about the set of JSON:API types used in JSON serialization of request/response bodies. +/// +[JsonSerializable(typeof(Document))] +public sealed partial class JsonApiSerializationContext : JsonSerializerContext +{ +} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs index 32e4351e12..97d7589cc6 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs @@ -7,24 +7,12 @@ 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); - } + JsonSerializer.Serialize(writer, value, options); } protected static JsonException GetEndOfStreamError() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index 2f40aeb27b..87c3a0acfa 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,10 +1,12 @@ using System.Text.Json.Serialization; +using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects; /// /// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure. /// +[PublicAPI] public sealed class Document { [JsonPropertyName("jsonapi")] diff --git a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs index 0942683487..0282ab39bd 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs @@ -80,7 +80,7 @@ private Document DeserializeDocument(string requestBody) using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - var document = JsonSerializer.Deserialize(requestBody, _options.SerializerReadOptions); + Document? document = JsonSerializer.Deserialize(requestBody, _options.SerializationReadContext.Document); AssertHasDocument(document, requestBody); diff --git a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs index 20f4ad242b..2bfefde98b 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs @@ -125,7 +125,7 @@ private string SerializeDocument(Document document) { using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - return JsonSerializer.Serialize(document, _options.SerializerWriteOptions); + return JsonSerializer.Serialize(document, _options.SerializationWriteContext.Document); } private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent) diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs index d9459f7ec1..f0af041de5 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs @@ -38,7 +38,7 @@ public void Resources_in_deeply_nested_circular_chain_are_written_in_relationshi Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); text.Should().BeJson(@"{ ""data"": { @@ -175,7 +175,7 @@ public void Resources_in_deeply_nested_circular_chains_are_written_in_relationsh }); // Assert - string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); text.Should().BeJson(@"{ ""data"": [ @@ -333,7 +333,7 @@ public void Resources_in_overlapping_deeply_nested_circular_chains_are_written_i Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); + string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); text.Should().BeJson(@"{ ""data"": { From f760e8f789b044ca35074b3ca5f84e084c422076 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sun, 21 Aug 2022 15:04:02 +0200 Subject: [PATCH 5/5] Revert "Use System.Text.Json source generator (see https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator)" This reverts commit a00ab50528ed6ba3db84d536287450d29b49a92d. --- .../DeserializationBenchmarkBase.cs | 6 ++-- .../OperationsDeserializationBenchmarks.cs | 2 +- .../ResourceDeserializationBenchmarks.cs | 2 +- .../OperationsSerializationBenchmarks.cs | 2 +- .../ResourceSerializationBenchmarks.cs | 2 +- .../SerializationBenchmarkBase.cs | 6 ++-- .../Configuration/IJsonApiOptions.cs | 17 ----------- .../Configuration/JsonApiOptions.cs | 23 ++++++--------- .../Middleware/JsonApiMiddleware.cs | 28 +++++++++---------- .../JsonApiSerializationContext.cs | 17 ----------- .../JsonConverters/JsonObjectConverter.cs | 14 +++++++++- .../Serialization/Objects/Document.cs | 2 -- .../Serialization/Request/JsonApiReader.cs | 2 +- .../Serialization/Response/JsonApiWriter.cs | 2 +- .../Response/ResponseModelAdapterTests.cs | 6 ++-- 15 files changed, 49 insertions(+), 82 deletions(-) delete mode 100644 src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs diff --git a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs index 80a9753597..bbf746d1a8 100644 --- a/benchmarks/Deserialization/DeserializationBenchmarkBase.cs +++ b/benchmarks/Deserialization/DeserializationBenchmarkBase.cs @@ -1,10 +1,10 @@ using System.ComponentModel.Design; +using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; using JsonApiDotNetCore.Serialization.Request.Adapters; using Microsoft.Extensions.Logging.Abstractions; @@ -13,7 +13,7 @@ namespace Benchmarks.Deserialization; public abstract class DeserializationBenchmarkBase { - protected readonly JsonApiSerializationContext SerializationReadContext; + protected readonly JsonSerializerOptions SerializerReadOptions; protected readonly DocumentAdapter DocumentAdapter; protected DeserializationBenchmarkBase() @@ -21,7 +21,7 @@ protected DeserializationBenchmarkBase() var options = new JsonApiOptions(); IResourceGraph resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph)); - SerializationReadContext = ((IJsonApiOptions)options).SerializationReadContext; + SerializerReadOptions = ((IJsonApiOptions)options).SerializerReadOptions; var serviceContainer = new ServiceContainer(); var resourceFactory = new ResourceFactory(serviceContainer); diff --git a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs index efe0ae568f..99adce73cb 100644 --- a/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/OperationsDeserializationBenchmarks.cs @@ -270,7 +270,7 @@ public class OperationsDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeOperationsRequest() { - Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; + var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs index 3d2cdd35af..e503a329bb 100644 --- a/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs +++ b/benchmarks/Deserialization/ResourceDeserializationBenchmarks.cs @@ -133,7 +133,7 @@ public class ResourceDeserializationBenchmarks : DeserializationBenchmarkBase [Benchmark] public object? DeserializeResourceRequest() { - Document document = JsonSerializer.Deserialize(RequestBody, SerializationReadContext.Document)!; + var document = JsonSerializer.Deserialize(RequestBody, SerializerReadOptions)!; return DocumentAdapter.Convert(document); } diff --git a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs index 2be9da5da6..471c9604c7 100644 --- a/benchmarks/Serialization/OperationsSerializationBenchmarks.cs +++ b/benchmarks/Serialization/OperationsSerializationBenchmarks.cs @@ -116,7 +116,7 @@ private static IEnumerable CreateResponseOperations(IJsonApi public string SerializeOperationsResponse() { Document responseDocument = ResponseModelAdapter.Convert(_responseOperations); - return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); + return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs index f896846ee2..a985bd5936 100644 --- a/benchmarks/Serialization/ResourceSerializationBenchmarks.cs +++ b/benchmarks/Serialization/ResourceSerializationBenchmarks.cs @@ -107,7 +107,7 @@ private static OutgoingResource CreateResponseResource() public string SerializeResourceResponse() { Document responseDocument = ResponseModelAdapter.Convert(ResponseResource); - return JsonSerializer.Serialize(responseDocument, SerializationWriteContext.Document); + return JsonSerializer.Serialize(responseDocument, SerializerWriteOptions); } protected override JsonApiRequest CreateJsonApiRequest(IResourceGraph resourceGraph) diff --git a/benchmarks/Serialization/SerializationBenchmarkBase.cs b/benchmarks/Serialization/SerializationBenchmarkBase.cs index be1a711ad9..d9cfefd0b6 100644 --- a/benchmarks/Serialization/SerializationBenchmarkBase.cs +++ b/benchmarks/Serialization/SerializationBenchmarkBase.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; using Benchmarks.Tools; using JetBrains.Annotations; @@ -7,7 +8,6 @@ using JsonApiDotNetCore.Queries.Internal; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Response; using Microsoft.Extensions.Logging.Abstractions; @@ -15,7 +15,7 @@ namespace Benchmarks.Serialization; public abstract class SerializationBenchmarkBase { - protected readonly JsonApiSerializationContext SerializationWriteContext; + protected readonly JsonSerializerOptions SerializerWriteOptions; protected readonly IResponseModelAdapter ResponseModelAdapter; protected readonly IResourceGraph ResourceGraph; @@ -33,7 +33,7 @@ protected SerializationBenchmarkBase() }; ResourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance).Add().Build(); - SerializationWriteContext = ((IJsonApiOptions)options).SerializationWriteContext; + SerializerWriteOptions = ((IJsonApiOptions)options).SerializerWriteOptions; // ReSharper disable VirtualMemberCallInConstructor JsonApiRequest request = CreateJsonApiRequest(ResourceGraph); diff --git a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs index 477500d79b..597d22294d 100644 --- a/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs @@ -1,8 +1,6 @@ using System.Data; using System.Text.Json; -using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; namespace JsonApiDotNetCore.Configuration; @@ -10,7 +8,6 @@ namespace JsonApiDotNetCore.Configuration; /// /// Global options that configure the behavior of JsonApiDotNetCore. /// -[PublicAPI] public interface IJsonApiOptions { /// @@ -159,27 +156,13 @@ public interface IJsonApiOptions /// JsonSerializerOptions SerializerOptions { get; } - /// - /// Gets the source-generated JSON serialization context used for deserializing request bodies. This value is based on - /// and is intended for internal use. - /// - JsonApiSerializationContext SerializationReadContext { get; } - /// /// Gets the settings used for deserializing request bodies. This value is based on and is intended for internal use. /// - [Obsolete("Use SerializationReadContext.Options instead.")] JsonSerializerOptions SerializerReadOptions { get; } - /// - /// Gets the source-generated JSON serialization context used for serializing response bodies. This value is based on - /// and is intended for internal use. - /// - JsonApiSerializationContext SerializationWriteContext { get; } - /// /// Gets the settings used for serializing response bodies. This value is based on and is intended for internal use. /// - [Obsolete("Use SerializationWriteContext.Options instead.")] JsonSerializerOptions SerializerWriteOptions { get; } } diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs index bb167e4db2..2e7cc54282 100644 --- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs +++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs @@ -3,7 +3,6 @@ using System.Text.Json; using JetBrains.Annotations; using JsonApiDotNetCore.Resources.Annotations; -using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.JsonConverters; namespace JsonApiDotNetCore.Configuration; @@ -12,20 +11,14 @@ namespace JsonApiDotNetCore.Configuration; [PublicAPI] public sealed class JsonApiOptions : IJsonApiOptions { - private readonly Lazy _lazySerializerReadContext; - private readonly Lazy _lazySerializerWriteContext; + private readonly Lazy _lazySerializerWriteOptions; + private readonly Lazy _lazySerializerReadOptions; /// - JsonApiSerializationContext IJsonApiOptions.SerializationReadContext => _lazySerializerReadContext.Value; + JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value; /// - JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => ((IJsonApiOptions)this).SerializationReadContext.Options; - - /// - JsonApiSerializationContext IJsonApiOptions.SerializationWriteContext => _lazySerializerWriteContext.Value; - - /// - JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => ((IJsonApiOptions)this).SerializationWriteContext.Options; + JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value; /// public string? Namespace { get; set; } @@ -117,16 +110,16 @@ static JsonApiOptions() public JsonApiOptions() { - _lazySerializerReadContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions)), - LazyThreadSafetyMode.ExecutionAndPublication); + _lazySerializerReadOptions = + new Lazy(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.ExecutionAndPublication); - _lazySerializerWriteContext = new Lazy(() => new JsonApiSerializationContext(new JsonSerializerOptions(SerializerOptions) + _lazySerializerWriteOptions = new Lazy(() => new JsonSerializerOptions(SerializerOptions) { Converters = { new WriteOnlyDocumentConverter(), new WriteOnlyRelationshipObjectConverter() } - }), LazyThreadSafetyMode.ExecutionAndPublication); + }, LazyThreadSafetyMode.ExecutionAndPublication); } } diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs index 6ea3853e92..2e15e6ae9a 100644 --- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs @@ -4,7 +4,6 @@ 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; @@ -45,7 +44,7 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin using (CodeTimingSessionManager.Current.Measure("JSON:API middleware")) { - if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializationWriteContext)) + if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerWriteOptions)) { return; } @@ -55,8 +54,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin if (primaryResourceType != null) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializationWriteContext) || - !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializationWriteContext)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerWriteOptions)) { return; } @@ -67,8 +66,8 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } else if (IsRouteForOperations(routeValues)) { - if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializationWriteContext) || - !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializationWriteContext)) + if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions) || + !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerWriteOptions)) { return; } @@ -92,11 +91,11 @@ public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMappin } } - private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonApiSerializationContext serializationContext) + private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerOptions serializerOptions) { if (httpContext.Request.Headers.ContainsKey(HeaderNames.IfMatch)) { - await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(HttpStatusCode.PreconditionFailed) + await FlushResponseAsync(httpContext.Response, serializerOptions, new ErrorObject(HttpStatusCode.PreconditionFailed) { Title = "Detection of mid-air edit collisions using ETags is not supported.", Source = new ErrorSource @@ -121,14 +120,13 @@ private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, Jso : null; } - private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, - JsonApiSerializationContext serializationContext) + private static async Task ValidateContentTypeHeaderAsync(string allowedContentType, HttpContext httpContext, JsonSerializerOptions serializerOptions) { string? contentType = httpContext.Request.ContentType; if (contentType != null && contentType != allowedContentType) { - await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(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.", @@ -145,7 +143,7 @@ private static async Task ValidateContentTypeHeaderAsync(string allowedCon } private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue allowedMediaTypeValue, HttpContext httpContext, - JsonApiSerializationContext serializationContext) + JsonSerializerOptions serializerOptions) { string[] acceptHeaders = httpContext.Request.Headers.GetCommaSeparatedValues("Accept"); @@ -178,7 +176,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a if (!seenCompatibleMediaType) { - await FlushResponseAsync(httpContext.Response, serializationContext, new ErrorObject(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.", @@ -194,7 +192,7 @@ private static async Task ValidateAcceptHeaderAsync(MediaTypeHeaderValue a return true; } - private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiSerializationContext serializationContext, ErrorObject error) + private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonSerializerOptions serializerOptions, ErrorObject error) { httpResponse.ContentType = HeaderConstants.MediaType; httpResponse.StatusCode = (int)error.StatusCode; @@ -204,7 +202,7 @@ private static async Task FlushResponseAsync(HttpResponse httpResponse, JsonApiS Errors = error.AsList() }; - await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializationContext.Document); + await JsonSerializer.SerializeAsync(httpResponse.Body, errorDocument, serializerOptions); await httpResponse.Body.FlushAsync(); } diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs b/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs deleted file mode 100644 index 38180d9369..0000000000 --- a/src/JsonApiDotNetCore/Serialization/JsonApiSerializationContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Serialization; -using JsonApiDotNetCore.Serialization.Objects; - -namespace JsonApiDotNetCore.Serialization; - -// Workaround for https://youtrack.jetbrains.com/issue/RSRP-487028 -partial class JsonApiSerializationContext -{ -} - -/// -/// Provides compile-time metadata about the set of JSON:API types used in JSON serialization of request/response bodies. -/// -[JsonSerializable(typeof(Document))] -public sealed partial class JsonApiSerializationContext : JsonSerializerContext -{ -} diff --git a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs index 97d7589cc6..32e4351e12 100644 --- a/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs +++ b/src/JsonApiDotNetCore/Serialization/JsonConverters/JsonObjectConverter.cs @@ -7,12 +7,24 @@ 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) { - JsonSerializer.Serialize(writer, value, 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() diff --git a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs index 87c3a0acfa..2f40aeb27b 100644 --- a/src/JsonApiDotNetCore/Serialization/Objects/Document.cs +++ b/src/JsonApiDotNetCore/Serialization/Objects/Document.cs @@ -1,12 +1,10 @@ using System.Text.Json.Serialization; -using JetBrains.Annotations; namespace JsonApiDotNetCore.Serialization.Objects; /// /// See https://jsonapi.org/format/1.1/#document-top-level and https://jsonapi.org/ext/atomic/#document-structure. /// -[PublicAPI] public sealed class Document { [JsonPropertyName("jsonapi")] diff --git a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs index 0282ab39bd..0942683487 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/JsonApiReader.cs @@ -80,7 +80,7 @@ private Document DeserializeDocument(string requestBody) using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - Document? document = JsonSerializer.Deserialize(requestBody, _options.SerializationReadContext.Document); + var document = JsonSerializer.Deserialize(requestBody, _options.SerializerReadOptions); AssertHasDocument(document, requestBody); diff --git a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs index 2bfefde98b..20f4ad242b 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/JsonApiWriter.cs @@ -125,7 +125,7 @@ private string SerializeDocument(Document document) { using IDisposable _ = CodeTimingSessionManager.Current.Measure("JsonSerializer.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages); - return JsonSerializer.Serialize(document, _options.SerializationWriteContext.Document); + return JsonSerializer.Serialize(document, _options.SerializerWriteOptions); } private bool SetETagResponseHeader(HttpRequest request, HttpResponse response, string responseContent) diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs index f0af041de5..d9459f7ec1 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/Serialization/Response/ResponseModelAdapterTests.cs @@ -38,7 +38,7 @@ public void Resources_in_deeply_nested_circular_chain_are_written_in_relationshi Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); text.Should().BeJson(@"{ ""data"": { @@ -175,7 +175,7 @@ public void Resources_in_deeply_nested_circular_chains_are_written_in_relationsh }); // Assert - string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); text.Should().BeJson(@"{ ""data"": [ @@ -333,7 +333,7 @@ public void Resources_in_overlapping_deeply_nested_circular_chains_are_written_i Document document = responseModelAdapter.Convert(article); // Assert - string text = JsonSerializer.Serialize(document, options.SerializationWriteContext.Document); + string text = JsonSerializer.Serialize(document, new JsonSerializerOptions(options.SerializerWriteOptions)); text.Should().BeJson(@"{ ""data"": {