Skip to content

Instantly share code, notes, and snippets.

@maxreb
Created November 29, 2022 15:29
Show Gist options
  • Save maxreb/947dd57b159d82aa75ac6943d66679e5 to your computer and use it in GitHub Desktop.
Save maxreb/947dd57b159d82aa75ac6943d66679e5 to your computer and use it in GitHub Desktop.
Benchmark PopulateObject
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;
}
}
}
<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>
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);
}
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<PopulateObjectBenchmark>();
@eiriktsarpalis
Copy link

The assertion in line 20 will probably fail in cases where T is a struct.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment