-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Support resumable serialization in NullableConverter<T> #65524
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,73 +1,102 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Diagnostics; | ||
|
||
namespace System.Text.Json.Serialization.Converters | ||
{ | ||
internal sealed class NullableConverter<T> : JsonConverter<T?> where T : struct | ||
{ | ||
internal override ConverterStrategy ConverterStrategy { get; } | ||
krwq marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume the ConverterStrategy is determined by the value held by Nullable? I.e. it can be Value, Object, Collection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right. |
||
internal override Type? ElementType => typeof(T); | ||
public override bool HandleNull => true; | ||
|
||
// It is possible to cache the underlying converter since this is an internal converter and | ||
// an instance is created only once for each JsonSerializerOptions instance. | ||
private readonly JsonConverter<T> _converter; | ||
private readonly JsonConverter<T> _elementConverter; | ||
|
||
public NullableConverter(JsonConverter<T> elementConverter) | ||
{ | ||
_elementConverter = elementConverter; | ||
ConverterStrategy = elementConverter.ConverterStrategy; | ||
IsInternalConverterForNumberType = elementConverter.IsInternalConverterForNumberType; | ||
// temporary workaround for JsonConverter base constructor needing to access | ||
// ConverterStrategy when calculating `CanUseDirectReadOrWrite`. | ||
CanUseDirectReadOrWrite = elementConverter.ConverterStrategy == ConverterStrategy.Value; | ||
} | ||
|
||
internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out T? value) | ||
{ | ||
if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null) | ||
{ | ||
value = null; | ||
return true; | ||
} | ||
|
||
public NullableConverter(JsonConverter<T> converter) | ||
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; | ||
if (_elementConverter.TryRead(ref reader, typeof(T), options, ref state, out T element)) | ||
{ | ||
value = element; | ||
return true; | ||
} | ||
|
||
value = null; | ||
return false; | ||
krwq marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
internal override bool OnTryWrite(Utf8JsonWriter writer, T? value, JsonSerializerOptions options, ref WriteStack state) | ||
{ | ||
_converter = converter; | ||
IsInternalConverterForNumberType = converter.IsInternalConverterForNumberType; | ||
if (value is null) | ||
{ | ||
writer.WriteNullValue(); | ||
return true; | ||
} | ||
|
||
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; | ||
return _elementConverter.TryWrite(writer, value.Value, options, ref state); | ||
} | ||
|
||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
// We do not check _converter.HandleNull, as the underlying struct cannot be null. | ||
// A custom converter for some type T? can handle null. | ||
if (reader.TokenType == JsonTokenType.Null) | ||
{ | ||
return null; | ||
} | ||
|
||
T value = _converter.Read(ref reader, typeof(T), options); | ||
T value = _elementConverter.Read(ref reader, typeof(T), options); | ||
return value; | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) | ||
{ | ||
if (!value.HasValue) | ||
if (value is null) | ||
{ | ||
// We do not check _converter.HandleNull, as the underlying struct cannot be null. | ||
// A custom converter for some type T? can handle null. | ||
writer.WriteNullValue(); | ||
} | ||
else | ||
{ | ||
_converter.Write(writer, value.Value, options); | ||
_elementConverter.Write(writer, value.Value, options); | ||
} | ||
} | ||
|
||
internal override T? ReadNumberWithCustomHandling(ref Utf8JsonReader reader, JsonNumberHandling numberHandling, JsonSerializerOptions options) | ||
{ | ||
// We do not check _converter.HandleNull, as the underlying struct cannot be null. | ||
// A custom converter for some type T? can handle null. | ||
if (reader.TokenType == JsonTokenType.Null) | ||
{ | ||
return null; | ||
} | ||
|
||
T value = _converter.ReadNumberWithCustomHandling(ref reader, numberHandling, options); | ||
T value = _elementConverter.ReadNumberWithCustomHandling(ref reader, numberHandling, options); | ||
return value; | ||
} | ||
|
||
internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? value, JsonNumberHandling handling) | ||
{ | ||
if (!value.HasValue) | ||
if (value is null) | ||
{ | ||
// We do not check _converter.HandleNull, as the underlying struct cannot be null. | ||
// A custom converter for some type T? can handle null. | ||
writer.WriteNullValue(); | ||
} | ||
else | ||
{ | ||
_converter.WriteNumberWithCustomHandling(writer, value.Value, handling); | ||
_elementConverter.WriteNumberWithCustomHandling(writer, value.Value, handling); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -227,7 +227,7 @@ internal bool TryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSeriali | |
JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo; | ||
#endif | ||
state.Push(); | ||
Debug.Assert(TypeToConvert.IsAssignableFrom(state.Current.JsonTypeInfo.Type)); | ||
Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strengthens an assertion that should have been changed via #65224. |
||
|
||
#if !DEBUG | ||
// For performance, only perform validation on internal converters on debug builds. | ||
|
@@ -462,7 +462,7 @@ internal bool TryWrite(Utf8JsonWriter writer, in T value, JsonSerializerOptions | |
JsonTypeInfo originalJsonTypeInfo = state.Current.JsonTypeInfo; | ||
#endif | ||
state.Push(); | ||
Debug.Assert(TypeToConvert.IsAssignableFrom(state.Current.JsonTypeInfo.Type)); | ||
Debug.Assert(TypeToConvert == state.Current.JsonTypeInfo.Type); | ||
|
||
if (!isContinuation) | ||
{ | ||
|
@@ -528,7 +528,7 @@ internal bool TryWriteDataExtensionProperty(Utf8JsonWriter writer, T value, Json | |
|
||
// Extension data properties change how dictionary key naming policies are applied. | ||
state.Current.IsWritingExtensionDataProperty = true; | ||
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; | ||
state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo; | ||
|
||
success = dictionaryConverter.OnWriteResume(writer, value, options, ref state); | ||
if (success) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,6 +74,29 @@ public async Task WriteNestedAsyncEnumerable_DTO<TElement>(IEnumerable<TElement> | |
Assert.Equal(1, asyncEnumerable.TotalDisposedEnumerators); | ||
} | ||
|
||
[Theory] | ||
[MemberData(nameof(GetAsyncEnumerableSources))] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be a non-IAsyncEnumerable test added that can verify a larger, nullable POCO (as a value type)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nullable POCOs still work, however they don't flow the state. This becomes more evident in the case of IAE which simply fails serialization. |
||
public async Task WriteNestedAsyncEnumerable_Nullable<TElement>(IEnumerable<TElement> source, int delayInterval, int bufferSize) | ||
{ | ||
// Primarily tests the ability of NullableConverter to flow async serialization state | ||
|
||
JsonSerializerOptions options = new JsonSerializerOptions | ||
{ | ||
DefaultBufferSize = bufferSize, | ||
IncludeFields = true, | ||
}; | ||
|
||
string expectedJson = await JsonSerializerWrapperForString.SerializeWrapper<(IEnumerable<TElement>, bool)?>((source, false), options); | ||
|
||
using var stream = new Utf8MemoryStream(); | ||
var asyncEnumerable = new MockedAsyncEnumerable<TElement>(source, delayInterval); | ||
await JsonSerializerWrapperForStream.SerializeWrapper<(IAsyncEnumerable<TElement>, bool)?>(stream, (asyncEnumerable, false), options); | ||
|
||
JsonTestHelper.AssertJsonEqual(expectedJson, stream.ToString()); | ||
Assert.Equal(1, asyncEnumerable.TotalCreatedEnumerators); | ||
Assert.Equal(1, asyncEnumerable.TotalDisposedEnumerators); | ||
} | ||
|
||
[Theory, OuterLoop] | ||
[InlineData(5000, 1000, true)] | ||
[InlineData(5000, 1000, false)] | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This renames a property I should have renamed in #65224 and matches the naming convention by the equivalent property in
ReadStackFrame
.