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

Migrate to system.text.json #2713

Merged
merged 93 commits into from
Aug 8, 2024
Merged

Migrate to system.text.json #2713

merged 93 commits into from
Aug 8, 2024

Conversation

kblok
Copy link
Member

@kblok kblok commented Jul 24, 2024

Description

Work in progress. Early review is welcome

Copy link
Contributor

@jnyrup jnyrup left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't look at the uses of attributes on types

Comment on lines 181 to 188
return JsonSerializer.Deserialize<TestExpectation[]>(fileContent, new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new JsonStringEnumMemberConverter(),
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: statically cache JsonSerializerOptions due to CA1869

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Comment on lines 14 to 15
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Primitives" Version="8.0.0" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we can remove these.
They are both transitively included by PuppeteerSharp.csproj's dependency on Microsoft.Extensions.Logging

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

public static TEnum FromValueString<TEnum>(string value)
where TEnum : struct, Enum
{
var enumValues = _stringToEnumCache.GetOrAdd(typeof(TEnum), type =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the two methods using _stringToEnumCache have slightly different delegates to GetOrAdd :suspect:

Perhaps out-of-scope for this PR, but we could use the trick from https://mariusgundersen.net/type-dictionary-trick/ to avoid constructing the ConcurrentDictionary and the lookup into it.

internal static class EnumHelper
{
    public static TEnum ToEnum<TEnum>(this string value)
        where TEnum : struct, Enum => StringToEnum<TEnum>.Cache[value];

    public static string ToValueString<TEnum>(this TEnum value)
        where TEnum : struct, Enum => EnumToString<TEnum>.Cache[value];

    public static TEnum FromValueString<TEnum>(string value)
        where TEnum : struct, Enum
    {
        if (StringToEnum<TEnum>.Cache.TryGetValue(value, out var enumValue))
        {
            return enumValue;
        }

        var defaultEnumAttribute = typeof(TEnum).GetCustomAttribute<DefaultEnumValueAttribute>();
        if (defaultEnumAttribute != null)
        {
            return (TEnum)(object)defaultEnumAttribute.Value;
        }

        throw new ArgumentException($"Unknown value '{value}' for enum {typeof(TEnum).Name}");
    }

    private static class StringToEnum<TEnum>
        where TEnum : struct, Enum
    {
        public static readonly IReadOnlyDictionary<string, TEnum> Cache = Compute();

        private static Dictionary<string, TEnum> Compute()
        {
            var names = Enum.GetNames(typeof(TEnum));
            var values = (TEnum[])Enum.GetValues(typeof(TEnum));
            var dictionary = new Dictionary<string, TEnum>(StringComparer.OrdinalIgnoreCase);
            for (var i = 0; i < names.Length; i++)
            {
                dictionary.Add(names[i], values[i]);
                var memberValue = typeof(TEnum).GetField(names[i]).GetCustomAttribute<EnumMemberAttribute>()?.Value;
                if (memberValue != null)
                {
                    dictionary[memberValue] = values[i];
                }
            }

            return dictionary;
        }
    }

    private static class EnumToString<TEnum>
        where TEnum : struct, Enum
    {
        public static readonly IReadOnlyDictionary<TEnum, string> Cache = Compute();

        private static Dictionary<TEnum, string> Compute()
        {
            var names = Enum.GetNames(typeof(TEnum));
            var dictionary = new Dictionary<TEnum, string>();
            foreach (var t in names)
            {
                var field = typeof(TEnum).GetField(t);
                var valueName = field.GetCustomAttribute<EnumMemberAttribute>()?.Value ?? t;
                var fieldValue = (TEnum)field.GetValue(null);
                dictionary[fieldValue] = valueName;
            }

            return dictionary;
        }
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up PR :)

Comment on lines 15 to 17
TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault
? new DefaultJsonTypeInfoResolver()
: SystemTextJsonSerializationContext.Default,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we always want to use SystemTextJsonSerializationContext.Default?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that with the conditional build that makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that with this change, I would be forcing people to use declared values in the Context.

In fact, I think that if we want Evaluate calls to work on AOT, you might need to use basic types or provide an extended context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm this. If the consumer doesn't enforce this. There is no need to do this.
If the consumer does enable this. They will also have to provide a Context for any extra types they use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine with me 👍
As a follow-up later, we could perhaps change it such that all types that don't rely on user types, are (de)serialized using a source-generated JSON serializer when using the .NET8 target.

/// <summary>
/// We need this class for AOT.
/// </summary>
[JsonSourceGenerationOptions(WriteIndented = true)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to write indented JSON?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not at all :)

@kblok
Copy link
Member Author

kblok commented Aug 7, 2024

@jnyrup some updates:

We can't set the JsonTypeInfoResolver by default. That would force the user to do the same thing.
If you build your project with AOT, and you use some special types when you call some Evaluate function. You have to provide a serialization context using Puppeteer.ExtraJsonSerializerContext. You should do that as a first step. After that, the DefaultJsonSerializerSettings will be cached.

I added a demo in the console app.

Thoughts?

@jnyrup
Copy link
Contributor

jnyrup commented Aug 7, 2024

I don't have that much experience with AOT and JsonTypeInfoResolver, so can't provide the best feedback here.

If I have two different types A and B and also two different source-generated JsonSerializerContexts, does the current design allow me two use them without using Puppeteer.ExtraJsonSerializerContext?

@kblok
Copy link
Member Author

kblok commented Aug 7, 2024

If I have two different types A and B and also two different source-generated JsonSerializerContexts, does the current design allow me two use them without using Puppeteer.ExtraJsonSerializerContext?

You could pass the result a JsonTypeInfoResolver.Combine() to Puppeteer.ExtraJsonSerializerContext.

@kblok kblok changed the base branch from master to v19 August 8, 2024 22:26
@kblok kblok merged commit e8eac9a into v19 Aug 8, 2024
14 checks passed
@kblok kblok deleted the migrate-to-system.text.json branch August 8, 2024 22:26
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.

4 participants