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

Fix issues related to JsonSerializerOptions mutation and caching. #66248

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,6 @@
<data name="SerializerContextOptionsImmutable" xml:space="preserve">
<value>A 'JsonSerializerOptions' instance associated with a 'JsonSerializerContext' instance cannot be mutated once the context has been instantiated.</value>
</data>
<data name="OptionsAlreadyBoundToContext" xml:space="preserve">
<value>"The specified 'JsonSerializerOptions' instance is already bound with a 'JsonSerializerContext' instance."</value>
</data>
<data name="ConverterForPropertyMustBeValid" xml:space="preserve">
<value>The generic type of the converter for property '{0}.{1}' must match with the specified converter type '{2}'. The converter must not be 'null'.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer
if (!state.SupportContinuation &&
jsonTypeInfo is JsonTypeInfo<T> info &&
info.SerializeHandler != null &&
info.Options._serializerContext?.CanUseSerializationLogic == true)
info.Options.JsonSerializerContext?.CanUseSerializationLogic == true)
{
info.SerializeHandler(writer, value);
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type run
Debug.Assert(runtimeType != null);

options ??= JsonSerializerOptions.Default;
if (!JsonSerializerOptions.IsInitializedForReflectionSerializer)
if (!options.IsInitializedForReflectionSerializer)
{
JsonSerializerOptions.InitializeForReflectionSerializer();
options.InitializeForReflectionSerializer();
}

return options.GetOrAddJsonTypeInfoForRootType(runtimeType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,9 +287,9 @@ public static partial class JsonSerializer
CancellationToken cancellationToken = default)
{
options ??= JsonSerializerOptions.Default;
if (!JsonSerializerOptions.IsInitializedForReflectionSerializer)
if (!options.IsInitializedForReflectionSerializer)
{
JsonSerializerOptions.InitializeForReflectionSerializer();
options.InitializeForReflectionSerializer();
}

return CreateAsyncEnumerableDeserializer(utf8Json, options, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private static void WriteUsingGeneratedSerializer<TValue>(Utf8JsonWriter writer,

if (jsonTypeInfo.HasSerialize &&
jsonTypeInfo is JsonTypeInfo<TValue> typedInfo &&
typedInfo.Options._serializerContext?.CanUseSerializationLogic == true)
typedInfo.Options.JsonSerializerContext?.CanUseSerializationLogic == true)
{
Debug.Assert(typedInfo.SerializeHandler != null);
typedInfo.SerializeHandler(writer, value);
Expand All @@ -59,8 +59,8 @@ private static void WriteUsingSerializer<TValue>(Utf8JsonWriter writer, in TValu

Debug.Assert(!jsonTypeInfo.HasSerialize ||
jsonTypeInfo is not JsonTypeInfo<TValue> ||
jsonTypeInfo.Options._serializerContext == null ||
!jsonTypeInfo.Options._serializerContext.CanUseSerializationLogic,
jsonTypeInfo.Options.JsonSerializerContext == null ||
!jsonTypeInfo.Options.JsonSerializerContext.CanUseSerializationLogic,
"Incorrect method called. WriteUsingGeneratedSerializer() should have been called instead.");

WriteStack state = default;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,7 @@ public abstract partial class JsonSerializerContext
/// <remarks>
/// The instance cannot be mutated once it is bound with the context instance.
/// </remarks>
public JsonSerializerOptions Options
{
get
{
if (_options == null)
{
_options = new JsonSerializerOptions();
_options._serializerContext = this;
}

return _options;
}
}
public JsonSerializerOptions Options => _options ??= new JsonSerializerOptions { JsonSerializerContext = this };

/// <summary>
/// Indicates whether pre-generated serialization logic for types in the context
Expand Down Expand Up @@ -95,13 +83,8 @@ protected JsonSerializerContext(JsonSerializerOptions? options)
{
if (options != null)
{
if (options._serializerContext != null)
{
ThrowHelper.ThrowInvalidOperationException_JsonSerializerOptionsAlreadyBoundToContext();
}

options.JsonSerializerContext = this;
_options = options;
options._serializerContext = this;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ internal void ClearCaches()
private void InitializeCachingContext()
{
_cachingContext = TrackedCachingContexts.GetOrCreate(this);
if (IsInitializedForReflectionSerializer)
{
_cachingContext.Options.IsInitializedForReflectionSerializer = true;
}
}

/// <summary>
Expand Down Expand Up @@ -159,7 +163,12 @@ public static CachingContext GetOrCreate(JsonSerializerOptions options)

// Use a defensive copy of the options instance as key to
// avoid capturing references to any caching contexts.
var key = new JsonSerializerOptions(options) { _serializerContext = options._serializerContext };
var key = new JsonSerializerOptions(options)
{
// Copy fields ignored by the copy constructor
// but are necessary to determine equivalence.
_serializerContext = options._serializerContext,
};
Debug.Assert(key._cachingContext == null);

ctx = new CachingContext(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,27 @@ public sealed partial class JsonSerializerOptions
// The global list of built-in converters that override CanConvert().
private static JsonConverter[]? s_defaultFactoryConverters;

// Stores the JsonTypeInfo factory, which requires unreferenced code and must be rooted by the reflection-based serializer.
private static Func<Type, JsonSerializerOptions, JsonTypeInfo>? s_typeInfoCreationFunc;

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
private static void RootReflectionSerializerDependencies()
{
if (s_defaultSimpleConverters is null)
{
s_defaultSimpleConverters = GetDefaultSimpleConverters();
s_defaultFactoryConverters = GetDefaultFactoryConverters();
s_typeInfoCreationFunc = CreateJsonTypeInfo;
}

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) => new JsonTypeInfo(type, options);
}

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
private static void RootBuiltInConverters()
private static JsonConverter[] GetDefaultFactoryConverters()
{
s_defaultSimpleConverters = GetDefaultSimpleConverters();
s_defaultFactoryConverters = new JsonConverter[]
return new JsonConverter[]
{
// Check for disallowed types.
new UnsupportedTypeConverterFactory(),
Expand Down Expand Up @@ -159,7 +175,7 @@ internal JsonConverter GetConverterFromMember(Type? parentClassType, Type proper
[RequiresUnreferencedCode("Getting a converter for a type may require reflection which depends on unreferenced code.")]
public JsonConverter GetConverter(Type typeToConvert!!)
{
RootBuiltInConverters();
RootReflectionSerializerDependencies();
return GetConverterInternal(typeToConvert);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,8 @@ public sealed partial class JsonSerializerOptions
/// </remarks>
public static JsonSerializerOptions Default { get; } = CreateDefaultImmutableInstance();

internal JsonSerializerContext? _serializerContext;

// Stores the JsonTypeInfo factory, which requires unreferenced code and must be rooted by the reflection-based serializer.
private static Func<Type, JsonSerializerOptions, JsonTypeInfo>? s_typeInfoCreationFunc;

// For any new option added, adding it to the options copied in the copy constructor below must be considered.

private JsonSerializerContext? _serializerContext;
private MemberAccessor? _memberAccessorStrategy;
private JsonNamingPolicy? _dictionaryKeyPolicy;
private JsonNamingPolicy? _jsonPropertyNamingPolicy;
Expand Down Expand Up @@ -143,19 +138,15 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this()
}

/// <summary>
/// Binds current <see cref="JsonSerializerOptions"/> instance with a new instance of the specified <see cref="JsonSerializerContext"/> type.
/// Binds current <see cref="JsonSerializerOptions"/> instance with a new instance of the specified <see cref="Serialization.JsonSerializerContext"/> type.
/// </summary>
/// <typeparam name="TContext">The generic definition of the specified context type.</typeparam>
/// <remarks>When serializing and deserializing types using the options
/// instance, metadata for the types will be fetched from the context instance.
/// </remarks>
public void AddContext<TContext>() where TContext : JsonSerializerContext, new()
{
if (_serializerContext != null)
{
ThrowHelper.ThrowInvalidOperationException_JsonSerializerOptionsAlreadyBoundToContext();
}

VerifyMutable();
TContext context = new();
_serializerContext = context;
context._options = this;
Expand Down Expand Up @@ -550,6 +541,16 @@ public ReferenceHandler? ReferenceHandler
}
}

internal JsonSerializerContext? JsonSerializerContext
{
get => _serializerContext;
set
{
VerifyMutable();
_serializerContext = value;
}
}

// The cached value used to determine if ReferenceHandler should use Preserve or IgnoreCycles semanitcs or None of them.
internal ReferenceHandlingStrategy ReferenceHandlingStrategy = ReferenceHandlingStrategy.None;

Expand All @@ -576,27 +577,25 @@ internal MemberAccessor MemberAccessorStrategy
}

/// <summary>
/// Whether <see cref="InitializeForReflectionSerializer()"/> needs to be called.
/// Whether the options instance has been primed for reflection-based serialization.
/// </summary>
internal static bool IsInitializedForReflectionSerializer { get; set; }
internal bool IsInitializedForReflectionSerializer { get; private set; }

/// <summary>
/// Initializes the converters for the reflection-based serializer.
/// <seealso cref="InitializeForReflectionSerializer"/> must be checked before calling.
/// </summary>
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
internal static void InitializeForReflectionSerializer()
internal void InitializeForReflectionSerializer()
{
// For threading cases, the state that is set here can be overwritten.
RootBuiltInConverters();
s_typeInfoCreationFunc = CreateJsonTypeInfo;
RootReflectionSerializerDependencies();
IsInitializedForReflectionSerializer = true;

[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) => new JsonTypeInfo(type, options);
if (_cachingContext != null)
{
_cachingContext.Options.IsInitializedForReflectionSerializer = true;
}
}


private JsonTypeInfo GetJsonTypeInfoFromContextOrCreate(Type type)
{
JsonTypeInfo? info = _serializerContext?.GetTypeInfo(type);
Expand All @@ -605,12 +604,13 @@ private JsonTypeInfo GetJsonTypeInfoFromContextOrCreate(Type type)
return info;
}

if (s_typeInfoCreationFunc == null)
if (!IsInitializedForReflectionSerializer)
{
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type);
return null!;
}

Debug.Assert(s_typeInfoCreationFunc != null);
return s_typeInfoCreationFunc(type, this);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ internal void InitializePropCache()
Debug.Assert(PropertyCache == null);
Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object);

JsonSerializerContext? context = Options._serializerContext;
JsonSerializerContext? context = Options.JsonSerializerContext;
Debug.Assert(context != null);

JsonPropertyInfo[] array;
Expand Down Expand Up @@ -630,7 +630,7 @@ internal void InitializeParameterCache()
Debug.Assert(PropertyCache != null);
Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object);

JsonSerializerContext? context = Options._serializerContext;
JsonSerializerContext? context = Options.JsonSerializerContext;
Debug.Assert(context != null);

JsonParameterInfoValues[] array;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,6 @@ internal static void ThrowUnexpectedMetadataException(
}
}

[DoesNotReturn]
public static void ThrowInvalidOperationException_JsonSerializerOptionsAlreadyBoundToContext()
{
throw new InvalidOperationException(SR.Format(SR.OptionsAlreadyBoundToContext));
}

[DoesNotReturn]
public static void ThrowNotSupportedException_BuiltInConvertersNotRooted(Type type)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public static void Converters_AndTypeInfoCreator_NotRooted_WhenMetadataNotPresen
// This test uses reflection to:
// - Access JsonSerializerOptions.s_defaultSimpleConverters
// - Access JsonSerializerOptions.s_defaultFactoryConverters
// - Access JsonSerializerOptions.s_typeInfoCreationFunc
//
// If any of them changes, this test will need to be kept in sync.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,9 @@ static Func<JsonSerializerOptions, int> CreateCacheCountAccessor()
}
}

[ActiveIssue("https://github.com/dotnet/runtime/issues/66232", TargetFrameworkMonikers.NetFramework)]
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[MemberData(nameof(GetJsonSerializerOptions))]
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
public static void JsonSerializerOptions_ReuseConverterCaches()
{
// This test uses reflection to:
Expand Down Expand Up @@ -286,10 +286,12 @@ public static void JsonSerializerOptions_ReuseConverterCaches()
for (int i = 0; i < 5; i++)
{
var options2 = new JsonSerializerOptions(options);
Assert.True(equalityComparer.Equals(options2, originalCacheOptions));
Assert.Equal(equalityComparer.GetHashCode(options2), equalityComparer.GetHashCode(originalCacheOptions));
Assert.Null(getCacheOptions(options2));

JsonSerializer.Serialize(42, options2);

Assert.True(equalityComparer.Equals(options2, originalCacheOptions));
Assert.Equal(equalityComparer.GetHashCode(options2), equalityComparer.GetHashCode(originalCacheOptions));
Assert.Same(originalCacheOptions, getCacheOptions(options2));
}
}
Expand Down Expand Up @@ -318,7 +320,6 @@ public static IEnumerable<object[]> GetJsonSerializerOptions()
}

[Fact]
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
public static void JsonSerializerOptions_EqualityComparer_ChangingAnySettingShouldReturnFalse()
{
// This test uses reflection to:
Expand Down Expand Up @@ -386,7 +387,6 @@ static IEnumerable<PropertyInfo> GetAllPublicPropertiesWithSetters()
}

[Fact]
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)]
public static void JsonSerializerOptions_EqualityComparer_ApplyingJsonSerializerContextShouldReturnFalse()
{
// This test uses reflection to:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// 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;
using System.IO;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;

namespace System.Text.Json.Serialization.Tests
Expand Down Expand Up @@ -176,6 +179,39 @@ void RunTest<TConverterReturn>()
}
}

[ActiveIssue("https://github.com/dotnet/runtime/issues/66232", TargetFrameworkMonikers.NetFramework)]
[ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
public static void GetConverter_Poco_WriteThrowsNotSupportedException()
{
RemoteExecutor.Invoke(() =>
{
JsonSerializerOptions options = new();
JsonConverter<Point_2D> converter = (JsonConverter<Point_2D>)options.GetConverter(typeof(Point_2D));

using var writer = new Utf8JsonWriter(new MemoryStream());
var value = new Point_2D(0, 0);

// Running the converter without priming the options instance
// for reflection-based serialization should throw NotSupportedException
// since it can't resolve reflection-based metadata.
Assert.Throws<NotSupportedException>(() => converter.Write(writer, value, options));
Debug.Assert(writer.BytesCommitted + writer.BytesPending == 0);

JsonSerializer.Serialize(42, options);

// Same operation should succeed when instance has been primed.
converter.Write(writer, value, options);
Debug.Assert(writer.BytesCommitted + writer.BytesPending > 0);
writer.Reset();

// State change should not leak into unrelated options instances.
var options2 = new JsonSerializerOptions();
options2.AddContext<JsonContext>();
Assert.Throws<NotSupportedException>(() => converter.Write(writer, value, options2));
Debug.Assert(writer.BytesCommitted + writer.BytesPending == 0);
}).Dispose();
}

[Fact]
public static void GetConverterTypeToConvertNull()
{
Expand Down
Loading