Skip to content

Commit

Permalink
Converted core code to use System.Text.Json
Browse files Browse the repository at this point in the history
- Added various converters to steer JsonSerializer in the right direction
- JsonApiDotNetCore.Serialization.Objects
  - Removed inheritance in JsonApiDotNetCore.Serialization.Objects, so we're in control of element write order
  - Moved "meta" to the end in all types (it is secondary information)
  - Consistently set IgnoreCondition on all properties, so we don't need to override global options anymore
  • Loading branch information
Bart Koelman committed Sep 14, 2021
1 parent 16ec48a commit e2c94d3
Show file tree
Hide file tree
Showing 103 changed files with 1,455 additions and 889 deletions.
3 changes: 3 additions & 0 deletions benchmarks/DependencyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ internal sealed class DependencyFactory
public IResourceGraph CreateResourceGraph(IJsonApiOptions options)
{
var builder = new ResourceGraphBuilder(options, NullLoggerFactory.Instance);

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

return builder.Build();
}
}
Expand Down
18 changes: 7 additions & 11 deletions benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Text.Json;
using BenchmarkDotNet.Attributes;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

namespace Benchmarks.Serialization
{
// ReSharper disable once ClassCanBeSealed.Global
[MarkdownExporter]
public class JsonApiDeserializerBenchmarks
{
private static readonly string Content = JsonConvert.SerializeObject(new Document
private static readonly string RequestBody = JsonSerializer.Serialize(new
{
Data = new ResourceObject
data = new
{
Type = BenchmarkResourcePublicNames.Type,
Id = "1",
Attributes = new Dictionary<string, object>
type = BenchmarkResourcePublicNames.Type,
id = "1",
attributes = new
{
["name"] = Guid.NewGuid().ToString()
}
}
});
Expand Down Expand Up @@ -55,7 +51,7 @@ public JsonApiDeserializerBenchmarks()
[Benchmark]
public object DeserializeSimpleObject()
{
return _jsonApiDeserializer.Deserialize(Content);
return _jsonApiDeserializer.Deserialize(RequestBody);
}
}
}
3 changes: 1 addition & 2 deletions src/Examples/GettingStarted/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;

namespace GettingStarted
{
Expand All @@ -21,7 +20,7 @@ public void ConfigureServices(IServiceCollection services)
options.Namespace = "api";
options.UseRelativeLinks = true;
options.IncludeTotalResourceCount = true;
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerOptions.WriteIndented = true;
});
}

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

namespace JsonApiDotNetCoreExample
{
Expand Down Expand Up @@ -52,8 +51,8 @@ public void ConfigureServices(IServiceCollection services)
options.UseRelativeLinks = true;
options.ValidateModelState = true;
options.IncludeTotalResourceCount = true;
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.Converters.Add(new StringEnumConverter());
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
#if DEBUG
options.IncludeExceptionStackTraceInErrors = true;
#endif
Expand Down
16 changes: 11 additions & 5 deletions src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Text.Json;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;
using Newtonsoft.Json;

namespace JsonApiDotNetCore.Configuration
{
Expand Down Expand Up @@ -136,11 +135,8 @@ public interface IJsonApiOptions
/// </summary>
IsolationLevel? TransactionIsolationLevel { get; }

JsonSerializerSettings SerializerSettings { get; }

/// <summary>
/// Specifies the settings that are used by the <see cref="System.Text.Json.JsonSerializer" />. Note that at some places a few settings are ignored, to
/// ensure JSON:API spec compliance.
/// Enables to customize the settings that are used by the <see cref="JsonSerializer" />.
/// </summary>
/// <example>
/// The next example sets the naming convention to camel casing.
Expand All @@ -150,5 +146,15 @@ public interface IJsonApiOptions
/// ]]></code>
/// </example>
JsonSerializerOptions SerializerOptions { get; }

/// <summary>
/// Gets the settings used for deserializing request bodies. This value is based on <see cref="SerializerOptions" /> and is intended for internal use.
/// </summary>
JsonSerializerOptions SerializerReadOptions { get; }

/// <summary>
/// Gets the settings used for serializing response bodies. This value is based on <see cref="SerializerOptions" /> and is intended for internal use.
/// </summary>
JsonSerializerOptions SerializerWriteOptions { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Building;
using JsonApiDotNetCore.Serialization.JsonConverters;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
Expand Down Expand Up @@ -87,6 +88,9 @@ public void AddResourceGraph(ICollection<Type> dbContextTypes, Action<ResourceGr
configureResourceGraph?.Invoke(_resourceGraphBuilder);

IResourceGraph resourceGraph = _resourceGraphBuilder.Build();

_options.SerializerOptions.Converters.Add(new ResourceObjectConverter(resourceGraph));

_services.AddSingleton(resourceGraph);
}

Expand Down
50 changes: 40 additions & 10 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
using System;
using System.Data;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading;
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using JsonApiDotNetCore.Serialization.JsonConverters;

namespace JsonApiDotNetCore.Configuration
{
/// <inheritdoc />
[PublicAPI]
public sealed class JsonApiOptions : IJsonApiOptions
{
private Lazy<JsonSerializerOptions> _lazySerializerWriteOptions;
private Lazy<JsonSerializerOptions> _lazySerializerReadOptions;

/// <inheritdoc />
JsonSerializerOptions IJsonApiOptions.SerializerReadOptions => _lazySerializerReadOptions.Value;

/// <inheritdoc />
JsonSerializerOptions IJsonApiOptions.SerializerWriteOptions => _lazySerializerWriteOptions.Value;

// Workaround for https://github.com/dotnet/efcore/issues/21026
internal bool DisableTopPagination { get; set; }
internal bool DisableChildrenPagination { get; set; }
Expand Down Expand Up @@ -72,19 +83,38 @@ public sealed class JsonApiOptions : IJsonApiOptions
/// <inheritdoc />
public IsolationLevel? TransactionIsolationLevel { get; set; }

public JsonSerializerSettings SerializerSettings { get; } = new()
/// <inheritdoc />
public JsonSerializerOptions SerializerOptions { get; } = new()
{
ContractResolver = new DefaultContractResolver
// These are the options common to serialization and deserialization.
// At runtime, we actually use SerializerReadOptions and SerializerWriteOptions, which are customized copies of these settings,
// to overcome the limitation in System.Text.Json that the JsonPath is incorrect when using custom converters.
// Therefore we try to avoid using custom converters has much as possible.
// https://github.com/Tarmil/FSharp.SystemTextJson/issues/37
// https://github.com/dotnet/runtime/issues/50205#issuecomment-808401245

PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters =
{
NamingStrategy = new CamelCaseNamingStrategy()
new SingleOrManyDataConverterFactory()
}
};

/// <inheritdoc />
public JsonSerializerOptions SerializerOptions { get; } = new()
public JsonApiOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
};
_lazySerializerReadOptions =
new Lazy<JsonSerializerOptions>(() => new JsonSerializerOptions(SerializerOptions), LazyThreadSafetyMode.PublicationOnly);

_lazySerializerWriteOptions = new Lazy<JsonSerializerOptions>(() => new JsonSerializerOptions(SerializerOptions)
{
Converters =
{
new WriteOnlyDocumentConverter(),
new WriteOnlyRelationshipObjectConverter()
}
}, LazyThreadSafetyMode.PublicationOnly);
}
}
}
5 changes: 3 additions & 2 deletions src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Mvc.ModelBinding;

Expand Down Expand Up @@ -117,8 +116,10 @@ private static ErrorObject FromModelError(ModelError modelError, string attribut

if (includeExceptionStackTraceInErrors && modelError.Exception != null)
{
string[] stackTraceLines = modelError.Exception.Demystify().ToString().Split(Environment.NewLine);

error.Meta ??= new Dictionary<string, object>();
error.Meta.IncludeExceptionStackTrace(modelError.Exception.Demystify());
error.Meta["StackTrace"] = stackTraceLines;
}

return error;
Expand Down
1 change: 0 additions & 1 deletion src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(EFCoreVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="$(EFCoreVersion)" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SauceControl.InheritDoc" Version="1.3.0" PrivateAssets="All" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>
Expand Down
5 changes: 3 additions & 2 deletions src/JsonApiDotNetCore/Middleware/ExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -102,8 +101,10 @@ private void ApplyOptions(ErrorObject error, Exception exception)

if (resultException != null && _options.IncludeExceptionStackTraceInErrors)
{
string[] stackTraceLines = resultException.ToString().Split(Environment.NewLine);

error.Meta ??= new Dictionary<string, object>();
error.Meta.IncludeExceptionStackTrace(resultException);
error.Meta["StackTrace"] = stackTraceLines;
}
}
}
Expand Down
Loading

0 comments on commit e2c94d3

Please sign in to comment.