Created
November 29, 2022 15:29
-
-
Save maxreb/947dd57b159d82aa75ac6943d66679e5 to your computer and use it in GitHub Desktop.
Benchmark PopulateObject
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics; | |
using System.Runtime.CompilerServices; | |
using System.Text.Json; | |
using System.Text.Json.Serialization.Metadata; | |
public static class JsonSerializerExt | |
{ | |
// Dynamically attach a JsonSerializerOptions copy that is configured using PopulateTypeInfoResolver | |
private readonly static ConditionalWeakTable<JsonSerializerOptions, JsonSerializerOptions> s_populateMap = new(); | |
public static void PopulateObject<T>(string json, T destination, JsonSerializerOptions? options = null) | |
{ | |
options = GetOptionsWithPopulateResolver(options); | |
Debug.Assert(options.TypeInfoResolver is PopulateTypeInfoResolver); | |
PopulateTypeInfoResolver.t_populateObject = destination; | |
try | |
{ | |
T? result = JsonSerializer.Deserialize<T>(json, options); | |
Debug.Assert(ReferenceEquals(result, destination)); | |
} | |
finally | |
{ | |
PopulateTypeInfoResolver.t_populateObject = null; | |
} | |
} | |
public static void PopulateObject(string json, Type returnType, object destination, JsonSerializerOptions? options = null) | |
{ | |
options = GetOptionsWithPopulateResolver(options); | |
PopulateTypeInfoResolver.t_populateObject = destination; | |
try | |
{ | |
object? result = JsonSerializer.Deserialize(json, returnType, options); | |
Debug.Assert(ReferenceEquals(result, destination)); | |
} | |
finally | |
{ | |
PopulateTypeInfoResolver.t_populateObject = null; | |
} | |
} | |
private static JsonSerializerOptions GetOptionsWithPopulateResolver(JsonSerializerOptions? options) | |
{ | |
options ??= JsonSerializerOptions.Default; | |
if (!s_populateMap.TryGetValue(options, out JsonSerializerOptions? populateResolverOptions)) | |
{ | |
JsonSerializer.Serialize(value: 0, options); // Force a serialization to mark options as read-only | |
Debug.Assert(options.TypeInfoResolver != null); | |
populateResolverOptions = new JsonSerializerOptions(options) | |
{ | |
TypeInfoResolver = new PopulateTypeInfoResolver(options.TypeInfoResolver) | |
}; | |
s_populateMap.TryAdd(options, populateResolverOptions); | |
} | |
return populateResolverOptions; | |
} | |
private class PopulateTypeInfoResolver : IJsonTypeInfoResolver | |
{ | |
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; | |
[ThreadStatic] internal static object? t_populateObject; | |
public PopulateTypeInfoResolver(IJsonTypeInfoResolver jsonTypeInfoResolver) | |
{ | |
_jsonTypeInfoResolver = jsonTypeInfoResolver; | |
} | |
public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) | |
{ | |
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(type, options); | |
if (typeInfo != null && typeInfo.Kind != JsonTypeInfoKind.None) | |
{ | |
Func<object>? defaultCreateObjectDelegate = typeInfo.CreateObject; | |
typeInfo.CreateObject = () => | |
{ | |
object? result = t_populateObject; | |
if (result != null) | |
{ | |
// clean up to prevent reuse in recursive scenaria | |
t_populateObject = null; | |
} | |
else | |
{ | |
// fall back to the default delegate | |
result = defaultCreateObjectDelegate?.Invoke(); | |
} | |
return result!; | |
}; | |
} | |
return typeInfo; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net7.0</TargetFramework> | |
<Nullable>enable</Nullable> | |
<ImplicitUsings>enable</ImplicitUsings> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" /> | |
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> | |
</ItemGroup> | |
</Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using BenchmarkDotNet.Attributes; | |
using Newtonsoft.Json; | |
[MemoryDiagnoser] | |
public class PopulateObjectBenchmark | |
{ | |
[Benchmark] | |
public void PopulateClass_FromInt_WithNewtonsoft() | |
{ | |
var value = new MyTestClass("init"); | |
JsonConvert.PopulateObject("""{"Value":42}""", value); | |
CheckValues(value); | |
} | |
[Benchmark] | |
public void PopulateClass_FromInt_WithSystemTextJson() | |
{ | |
var value = new MyTestClass("init"); | |
JsonSerializerExt.PopulateObject("""{"Value":42}""", value); | |
CheckValues(value); | |
} | |
[Benchmark] | |
public void PopulateClass_FromInt_WithSystemTextJson_AsObject() | |
{ | |
var value = new MyTestClass("init"); | |
JsonSerializerExt.PopulateObject("""{"Value":42}""", typeof(MyTestClass), value); | |
CheckValues(value); | |
} | |
private static void CheckValues(MyTestClass value) | |
{ | |
if (value.Value != 42) | |
throw new Exception("'Value' is not '42'"); | |
if (value.Text != "init") | |
throw new Exception("'Text' is not 'init'"); | |
} | |
private record class MyTestClass(string Text, int Value = 0); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using BenchmarkDotNet.Running; | |
BenchmarkRunner.Run<PopulateObjectBenchmark>(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The assertion in line 20 will probably fail in cases where
T
is a struct.