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

Make contexts more combinator friendly #9

Merged

Conversation

krwq
Copy link
Owner

@krwq krwq commented Jun 9, 2022

  • we don't need new API in JsonSerializeContext
  • we do not need to create new instance of context inside of context.GetTypeInfo
  • converters work more consistently
  • you can customize child type info of contexts now if you combine (previously it was hardcoded)

@krwq krwq force-pushed the contract-customization-sourcegen-improvements branch from adedb6b to 93cbac4 Compare June 10, 2022 11:02
{
// Only cache the value once (de)serialization has occurred since new converters can be added that may change the result.
if (_cachingContext != null)
JsonConverter? converter = GetOrAddJsonTypeInfoEx(typeToConvert, configured: false, doNotThrow: true)?.Converter;
Copy link
Collaborator

@eiriktsarpalis eiriktsarpalis Jun 13, 2022

Choose a reason for hiding this comment

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

If this method is "NotCached", why is it calling into GetOrAddJsonTypeInfoEx which does consult the cache? It basically means that a cache miss will result in multiple lookups for no clear reason.

Copy link
Owner Author

Choose a reason for hiding this comment

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

NotCached refers to converter cache not JsonTypeInfo cache

Copy link
Owner Author

Choose a reason for hiding this comment

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

I'll add a comment on top to make that clear

Copy link
Owner Author

Choose a reason for hiding this comment

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

        /// <summary>
        /// Gets converter from type info without using converter cache.
        /// This method is used by converter cache. It may use type info cache.
        /// </summary>

Copy link
Collaborator

Choose a reason for hiding this comment

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

Here's a question: do we need to keep the converter cache if we're moving the source truth for converters to metadata?

Copy link
Owner Author

@krwq krwq Jun 14, 2022

Choose a reason for hiding this comment

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

For regular scenario it's technically not needed but:

  • GetConverter also needs to return answer from options and reflection even if type info is null (for compat)
  • we previously cached instance which technically means we always got exactly same instance after first usage, if I removed the converter caching it will still be cached if resolver returned non-null though so that might be ok

I'm ok with removing that but we need to agree above is ok

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it's ok to return a non-cached converter for GetConverter call not backed by JsonTypeInfo. Are there any cases in the hot path where it might result in unnecessary recalculations?

Copy link
Owner Author

Choose a reason for hiding this comment

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

no, we're gonna cache everything in the hot paths; will remove converter cache


if (converter == null)
{
ThrowHelper.ThrowNotSupportedException_BuiltInConvertersNotRooted(typeToConvert);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This seems wrong. Why would we throw this error if JsonSerializerOptions.Converters doesn't contain a compatible converter? As the name suggests the error is meant to be thrown if the built-in converters have not been rooted.

Copy link
Owner Author

Choose a reason for hiding this comment

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

As I mentioned in other places, we don't throw here unless:

  • rooting logic was never exercised
  • we somehow ended up using resolver which source gen did not generate type info code for (that's what we also did before this change) - same applies if multiple contexts are combined

return converter;
}

private JsonConverter? GetConverterFromOptions(Type typeToConvert)
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the current behaviour we want to return the built-in converters (or throw an exception if they haven't been rooted yet). This seems like a breaking change?

Copy link
Owner Author

Choose a reason for hiding this comment

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

it's not breaking change. If you call GetConverter directly it will root converter first and then call this method will behave as it was.

On the other hand if this is called when context is bound but converters are not rooted (it may be called implicitly) you will get an exception same way as you used to.

}

internal JsonConverter GetConverterFromType(Type typeToConvert)
private JsonConverter GetConverterFromOptionsOrReflectionConverter(Type typeToConvert)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Calling it ResolverConverter over GetConverter might be better at emphasizing this is not caching

Suggested change
private JsonConverter GetConverterFromOptionsOrReflectionConverter(Type typeToConvert)
private JsonConverter ResolveConverter(Type typeToConvert)

Copy link
Owner Author

Choose a reason for hiding this comment

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

this actually isn't resolving converter - this is technically speaking reflection converter logic. From user perspective the order is following:

  • type info converter (i.e. serializer context)
  • options
  • reflection converter (if rooted, if GetConverter called explicitly then this is always rooted)

this method does the last two and it's used by: reflection json type info and as a fallback for GetConverter (the fallback is arguably wrong but since we already shipped like that this logic is preserved here)

Copy link
Collaborator

Choose a reason for hiding this comment

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

this actually isn't resolving converter

Arguably it's resolving a converter based on the Converters list and the built-in stuff. My only concern is that we have accumulated too many JsonSerializer.GetConverter* method names and it might be in our interest to adopt a separate naming convention for the converter resolution logic that doesn't involve the cache in any way.

Copy link
Owner Author

@krwq krwq Jun 14, 2022

Choose a reason for hiding this comment

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

There are only two case which must not use cache and first one could also cause stack overflow: reflection/custom type info resolver which needs to get converter; calling GetConverter before options are locked (caching cannot happen because converters/type infos can still be changed). Everything else is should go through caching

}

JsonConverter<T> underlyingConverter = GetTypedConverter<T>(options.GetConverterFromTypeInfo(typeof(T)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

This kind of lookup seems completely out of place compared to the other JsonMetadataServices converter components. I'm concerned it might be accidentally rooting reflection components in sourcegen. cc @layomia

Copy link
Owner Author

Choose a reason for hiding this comment

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

it won't. The only place we do rooting is public GetConverter. This will lead to exception unless you called one of the rooting APIs. This is the reason I refactored GetConverter* methods so that we preserve this behavior

Copy link
Owner Author

Choose a reason for hiding this comment

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

another thing to note here is that there are actually only 4 places we get inner converters:

  • nullable
  • collections
  • enumerables
  • objects

we do exactly same for all 3 others except it's more indirect

Copy link
Collaborator

Choose a reason for hiding this comment

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

we do exactly same for all 3 others except it's more indirect

That's my point actually. The source generator is very intentionally making it so that metadata are composed explicitly rather than performing lookups via indirection to the metadata resolver. Why does nullable need to be different and does it imply that nested metadata resolution semantics diverge compared to, say, collections?

Copy link
Owner Author

Choose a reason for hiding this comment

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

this indirection happens exactly once per type info, later it's cached forever. See this test case: https://github.com/krwq/runtime-1/pull/9/files#diff-9ca8a434d4ee452728961d1242d8680e5f4b72c8973a99b7ef59ecb005541025R131
this is not arguable what the expected behavior should be.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not disputing that, it's more a question of why this specific type needing to be different. It seems weird that it does and introduces more accidental complexity. Isn't it possible to emulate prior art?

Copy link
Owner Author

@krwq krwq Jun 20, 2022

Choose a reason for hiding this comment

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

this is called from generated code and technically speaking we could call options.GetConverter but at the same time that currently roots reflection converters - if we do that that would break linker safety

@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkMinimum)</TargetFrameworks>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- SYSLIB0020: JsonSerializerOptions.IgnoreNullValues is obsolete -->
<NoWarn>$(NoWarn);SYSLIB0020</NoWarn>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Tests validating the new behavior?

Copy link
Owner Author

Choose a reason for hiding this comment

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

I'll add them as part of Combine tests

Copy link
Owner Author

Choose a reason for hiding this comment

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

I've added one larger test here https://github.com/krwq/runtime-1/pull/9/files#diff-9ca8a434d4ee452728961d1242d8680e5f4b72c8973a99b7ef59ecb005541025R131 exercising all trickier cases. Everything else should be covered by existing test cases

@krwq krwq force-pushed the contract-customization branch from e7b982b to bb56c48 Compare June 14, 2022 09:56
@krwq krwq force-pushed the contract-customization-sourcegen-improvements branch 2 times, most recently from 9d39911 to e3fe6ba Compare June 14, 2022 10:43
@krwq krwq force-pushed the contract-customization-sourcegen-improvements branch from e3fe6ba to 1a913d4 Compare June 20, 2022 07:40
@krwq
Copy link
Owner Author

krwq commented Jun 20, 2022

I've pushed a rebased version, will address the comment about removing converter cache as separate commit

@krwq krwq merged this pull request into contract-customization Jun 20, 2022
krwq added a commit that referenced this pull request Jun 20, 2022
fix build errors

Move converter rooting to DefaultJsonTypeInfoResolver so that it can be used standalone

Fix ConfigurationList.IsReadOnly

Minor refactorings (#1)

* Makes the following changes:

* Move singleton initialization for DefaultTypeInfoResolver behind a static property.
* Consolidate JsonSerializerContext & IJsonTypeInfoResolver values to a single field.
* Move reflection fallback logic away from JsonSerializerContext and into JsonSerializerOptions

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs

* remove testing of removed field

Simplify the JsonTypeInfo.CreateObject implemenetation (#2)

* Simplify the JsonTypeInfo.CreateObject implemenetation

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs

Co-authored-by: Krzysztof Wicher <[email protected]>

Co-authored-by: Krzysztof Wicher <[email protected]>

Tests and fixes for JsonTypeInfoKind.None

TypeInfo type mismatch tests

Allow setting NumberHandling on JsonTypeInfoKind.None

test resolver returning wrong type of options

JsonTypeInfo/JsonPropertyInfo mutability tests

rename test file

Move default converter rooting responsibility behind DefaultJsonTypeInfoResolver (#3)

* Move default converter rooting responsibility behind DefaultJsonTypeInfoResolver

* address feedback

Add simple test for using JsonTypeInfo<T> with APIs directly taking it

fix and tests for untyped/typed CreateObject

uncomment test cases, remove todo

More tests and tiny fixes

Add a JsonTypeInfoResolver.Combine test for JsonSerializerContext (#4)

* Fix JsonTypeInfoResolver.Combine for JsonSerializerContext

* Break up failing test

Fix simple scenarios for combining contexts (#6)

* Fix simple scenarios for combining contexts

* feedback

JsonSerializerContext combine test with different camel casing

Remove unneeded virtual calls & branching when accessing Get & Set delegates (#7)

JsonPropertyInfo tests everything minus ShouldSerialize & NumberHandling

Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs

Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs

throw InvalidOperationException rather than ArgumentNullException for source gen when PropertyInfo.Name is assigned through JsonPropertyInfoValues

tests for duplicated property names and JsonPropertyInfo.NumberHandling

Add tests for NumberHandling and failing tests for ShouldSerialize

disable the failing test and add extra checks

disable remainder of the failing ShouldSerialize tests, fix working one

Fix ShouldSerialize and IgnoreCondition interop

Add failing tests for CreateObject + parametrized constructors

Fix CreateObject support for JsonConstructor types (#10)

* Fix CreateObject support for JsonConstructor types

* address feedback

Make contexts more combinator friendly (#9)

* Make contexts more combinator friendly

* remove converter cache
krwq added a commit that referenced this pull request Jun 27, 2022
* Initial implementation for contract customization

fix build errors

Move converter rooting to DefaultJsonTypeInfoResolver so that it can be used standalone

Fix ConfigurationList.IsReadOnly

Minor refactorings (#1)

* Makes the following changes:

* Move singleton initialization for DefaultTypeInfoResolver behind a static property.
* Consolidate JsonSerializerContext & IJsonTypeInfoResolver values to a single field.
* Move reflection fallback logic away from JsonSerializerContext and into JsonSerializerOptions

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs

* remove testing of removed field

Simplify the JsonTypeInfo.CreateObject implemenetation (#2)

* Simplify the JsonTypeInfo.CreateObject implemenetation

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs

* Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.cs

Co-authored-by: Krzysztof Wicher <[email protected]>

Co-authored-by: Krzysztof Wicher <[email protected]>

Tests and fixes for JsonTypeInfoKind.None

TypeInfo type mismatch tests

Allow setting NumberHandling on JsonTypeInfoKind.None

test resolver returning wrong type of options

JsonTypeInfo/JsonPropertyInfo mutability tests

rename test file

Move default converter rooting responsibility behind DefaultJsonTypeInfoResolver (#3)

* Move default converter rooting responsibility behind DefaultJsonTypeInfoResolver

* address feedback

Add simple test for using JsonTypeInfo<T> with APIs directly taking it

fix and tests for untyped/typed CreateObject

uncomment test cases, remove todo

More tests and tiny fixes

Add a JsonTypeInfoResolver.Combine test for JsonSerializerContext (#4)

* Fix JsonTypeInfoResolver.Combine for JsonSerializerContext

* Break up failing test

Fix simple scenarios for combining contexts (#6)

* Fix simple scenarios for combining contexts

* feedback

JsonSerializerContext combine test with different camel casing

Remove unneeded virtual calls & branching when accessing Get & Set delegates (#7)

JsonPropertyInfo tests everything minus ShouldSerialize & NumberHandling

Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs

Update src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs

throw InvalidOperationException rather than ArgumentNullException for source gen when PropertyInfo.Name is assigned through JsonPropertyInfoValues

tests for duplicated property names and JsonPropertyInfo.NumberHandling

Add tests for NumberHandling and failing tests for ShouldSerialize

disable the failing test and add extra checks

disable remainder of the failing ShouldSerialize tests, fix working one

Fix ShouldSerialize and IgnoreCondition interop

Add failing tests for CreateObject + parametrized constructors

Fix CreateObject support for JsonConstructor types (#10)

* Fix CreateObject support for JsonConstructor types

* address feedback

Make contexts more combinator friendly (#9)

* Make contexts more combinator friendly

* remove converter cache

* redesign test to account for JsonConstructorAttribute

* Combine unit tests

* address feedback

* Add acceptance tests for DataContract attributes & Specified pattern (#11)

* Add private field serialization acceptance test (#13)

* tests, PR feedback (#14)

* PR feedback and extra tests

* Shorten class name, remove incorrect check (not true for polimorphic cases)

* Make parameter matching for custom properties map property Name with parameter (#16)

* Test static initialization with JsonTypeInfo (#17)

* Fix test failures and proper fix this time (#18)

* Fix test failures and proper fix this time

* reinstate ActiveIssueAttribute

* PR feedback and adjust couple of tests which don't set TypeInfoResolver

* fix IAsyncEnumerable tests

* Lock JsonSerializerOptions in JsonTypeInfo.EnsureConfigured()

Co-authored-by: Eirik Tsarpalis <[email protected]>

Co-authored-by: Eirik Tsarpalis <[email protected]>
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