Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JsonConverter generator #65

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

henrikwidlund
Copy link
Contributor

Adds generator for creating JsonConverters that uses the EnumExtensions to serialize and deserialize enum values.

#64

@henrikwidlund
Copy link
Contributor Author

This PR seems larger than it is, most of the files are generated by unit tests (see the Snapshots directory).

@andrewlock
Copy link
Owner

Hey, sorry for being so slow getting to this! This looks like a nice idea, I'm just not sure if it's out-of-scope for this library 🤔 Have you considered releasing it as a separate package?

@henrikwidlund
Copy link
Contributor Author

Hey, better late than never 😉 Feel free to close this if you feel it doesn't fit in this repo. I might consider releasing it as a separate package then.

@andrewlock
Copy link
Owner

I'm torn, honestly! 😀 Thing is, the built-in string enum converter already has more options for the conversion and caches everything aggressively in the constructor, so I'm not really sure that this generated converter would necessarily give much benefit over using that one... I would suggest trying to benchmark it first to see if it shows any improvement

@henrikwidlund
Copy link
Contributor Author

Now it's me that has to apologize for the late reply, I totally missed yours.
I've done some very basic benchmarks, and the results are mixed, some cases are a bit faster, some a bit slower. I'd say that the generic JsonStringEnumConverter<TEnum> is a better choice as long as you don't have to decorate your enum members with attributes for custom values.

My environment:

dotnet --info
.NET SDK:
 Version:           8.0.302
 Commit:            ef14e02af8
 Workload version:  8.0.300-manifests.f6879a9a
 MSBuild version:   17.10.4+10fbfbf2e

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  14.5
 OS Platform: Darwin
 RID:         osx-arm64
 Base Path:   /usr/local/share/dotnet/sdk/8.0.302/

.NET workloads installed:
There are no installed workloads to display.

Host:
  Version:      8.0.6
  Architecture: arm64
  Commit:       3b8b000a0e

Benchmark results (MacBook Pro M1):

Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
JsonStringEnumConverter 231.2 ns 0.30 ns 0.27 ns 1.00 0.0176 112 B 1.00
JsonStringEnumConverter2 226.6 ns 0.32 ns 0.30 ns 0.98 0.0176 112 B 1.00
JsonStringEnumConverterSpan 224.0 ns 0.29 ns 0.25 ns 0.97 0.0176 112 B 1.00
JsonStringEnumConverterSpan2 223.1 ns 0.31 ns 0.29 ns 0.96 0.0176 112 B 1.00
EnumJsonConverter 193.0 ns 0.21 ns 0.19 ns 0.83 0.0176 112 B 1.00
EnumJsonConverter2 227.0 ns 1.04 ns 0.97 ns 0.98 0.0176 112 B 1.00
EnumJsonConverterSpan 190.8 ns 0.22 ns 0.21 ns 0.82 0.0176 112 B 1.00
EnumJsonConverterSpan2 225.8 ns 0.24 ns 0.22 ns 0.98 0.0176 112 B 1.00
Method Mean Error StdDev Ratio Gen0 Allocated Alloc Ratio
JsonStringEnumConverter 134.4 ns 0.99 ns 0.92 ns 1.00 0.0305 192 B 1.00
JsonStringEnumConverter2 134.9 ns 0.85 ns 0.79 ns 1.00 0.0305 192 B 1.00
EnumJsonConverter 145.5 ns 0.30 ns 0.28 ns 1.08 0.0126 80 B 0.42
EnumJsonConverter2 135.2 ns 0.22 ns 0.18 ns 1.01 0.0305 192 B 1.00

Code used for benchark:

using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using NetEscapades.EnumGenerators;

// Test

BenchmarkSwitcher
    .FromAssembly(typeof(Program).Assembly)
    .Run(args);

[EnumExtensions]
[EnumJsonConverter(typeof(TestEnumConverter), CaseSensitive = true, AllowMatchingMetadataAttribute = false)]
[JsonConverter(typeof(TestEnumConverter))]
public enum TestEnum
{
    First = 0,
    [Display(Name = "2nd")] Second = 1,
    Third = 2
}

[EnumExtensions]
[JsonConverter(typeof(JsonStringEnumConverter<TestEnum2>))]
public enum TestEnum2
{
    First = 0,
    [Display(Name = "2nd")] Second = 1,
    Third = 2
}

[MemoryDiagnoser]
public class DeserializeBenchmark
{
    private const string EnumsString = """
            ["Second","Third","First","Second"]
            """;

    private static readonly Memory<char> EnumsMemory = EnumsString.ToArray().AsMemory();

    private static readonly JsonSerializerOptions JsonSerializerOptions = new()
    {
        Converters = { new JsonStringEnumConverter() }
    };

    [Benchmark(Baseline = true)]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum[]? JsonStringEnumConverter() =>
        JsonSerializer.Deserialize<TestEnum[]>(EnumsString, JsonSerializerOptions);
    
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum2[]? JsonStringEnumConverter2() =>
        JsonSerializer.Deserialize<TestEnum2[]>(EnumsString, JsonSerializerOptions);

    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum[]? JsonStringEnumConverterSpan() =>
        JsonSerializer.Deserialize<TestEnum[]>(EnumsMemory.Span, JsonSerializerOptions);
    
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum2[]? JsonStringEnumConverterSpan2() =>
        JsonSerializer.Deserialize<TestEnum2[]>(EnumsMemory.Span, JsonSerializerOptions);

    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum[]? EnumJsonConverter() => JsonSerializer.Deserialize<TestEnum[]>(EnumsString);
    
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum2[]? EnumJsonConverter2() => JsonSerializer.Deserialize<TestEnum2[]>(EnumsString);

    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum[]? EnumJsonConverterSpan() => JsonSerializer.Deserialize<TestEnum[]>(EnumsMemory.Span);
    
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public TestEnum2[]? EnumJsonConverterSpan2() => JsonSerializer.Deserialize<TestEnum2[]>(EnumsMemory.Span);
}

[MemoryDiagnoser]
public class SerializeBenchmark
{
    private static readonly TestEnum[] BenchmarkEnums =
    {
        TestEnum.Second,
        TestEnum.Third,
        TestEnum.First,
        TestEnum.Second
    };
    
    private static readonly TestEnum2[] BenchmarkEnums2 =
    {
        TestEnum2.Second,
        TestEnum2.Third,
        TestEnum2.First,
        TestEnum2.Second
    };

    private static readonly JsonSerializerOptions JsonSerializerOptions = new()
    {
        Converters = { new JsonStringEnumConverter() }
    };

    [Benchmark(Baseline = true)]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public string JsonStringEnumConverter() => JsonSerializer.Serialize(BenchmarkEnums, JsonSerializerOptions);
    
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public string JsonStringEnumConverter2() => JsonSerializer.Serialize(BenchmarkEnums2, JsonSerializerOptions);

    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public string EnumJsonConverter() => JsonSerializer.Serialize(BenchmarkEnums);
    
    [Benchmark]
    [MethodImpl(MethodImplOptions.NoInlining)]
    public string EnumJsonConverter2() => JsonSerializer.Serialize(BenchmarkEnums2);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants