diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
index d24617f74a3d31..be653156bb277e 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -121,6 +121,7 @@ System.Text.Json.Nodes.JsonValue
+
@@ -247,6 +248,8 @@ System.Text.Json.Nodes.JsonValue
+
+
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs
index 2596023f6b0dd8..3ab98e043385da 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs
@@ -76,7 +76,7 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer
if (!state.SupportContinuation &&
jsonTypeInfo is JsonTypeInfo info &&
info.SerializeHandler != null &&
- info.Options._context?.CanUseSerializationLogic == true)
+ info.Options._serializerContext?.CanUseSerializationLogic == true)
{
info.SerializeHandler(writer, value);
return true;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs
index 40649ec846660a..fa39f8daaf871b 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Helpers.cs
@@ -18,12 +18,12 @@ private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type run
Debug.Assert(runtimeType != null);
options ??= JsonSerializerOptions.Default;
- if (!options.IsInitializedForReflectionSerializer)
+ if (!JsonSerializerOptions.IsInitializedForReflectionSerializer)
{
- options.InitializeForReflectionSerializer();
+ JsonSerializerOptions.InitializeForReflectionSerializer();
}
- return options.GetOrAddClassForRootType(runtimeType);
+ return options.GetOrAddJsonTypeInfoForRootType(runtimeType);
}
private static JsonTypeInfo GetTypeInfo(JsonSerializerContext context, Type type)
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs
index 46963245c89af2..c13ce252fee16a 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.Stream.cs
@@ -287,9 +287,9 @@ public static partial class JsonSerializer
CancellationToken cancellationToken = default)
{
options ??= JsonSerializerOptions.Default;
- if (!options.IsInitializedForReflectionSerializer)
+ if (!JsonSerializerOptions.IsInitializedForReflectionSerializer)
{
- options.InitializeForReflectionSerializer();
+ JsonSerializerOptions.InitializeForReflectionSerializer();
}
return CreateAsyncEnumerableDeserializer(utf8Json, options, cancellationToken);
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs
index 415f1417ae6071..45a39dc58511fe 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Write.Helpers.cs
@@ -41,7 +41,7 @@ private static void WriteUsingGeneratedSerializer(Utf8JsonWriter writer,
if (jsonTypeInfo.HasSerialize &&
jsonTypeInfo is JsonTypeInfo typedInfo &&
- typedInfo.Options._context?.CanUseSerializationLogic == true)
+ typedInfo.Options._serializerContext?.CanUseSerializationLogic == true)
{
Debug.Assert(typedInfo.SerializeHandler != null);
typedInfo.SerializeHandler(writer, value);
@@ -59,8 +59,8 @@ private static void WriteUsingSerializer(Utf8JsonWriter writer, in TValu
Debug.Assert(!jsonTypeInfo.HasSerialize ||
jsonTypeInfo is not JsonTypeInfo ||
- jsonTypeInfo.Options._context == null ||
- !jsonTypeInfo.Options._context.CanUseSerializationLogic,
+ jsonTypeInfo.Options._serializerContext == null ||
+ !jsonTypeInfo.Options._serializerContext.CanUseSerializationLogic,
"Incorrect method called. WriteUsingGeneratedSerializer() should have been called instead.");
WriteStack state = default;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs
index 0ce18180072c9e..7254cd6f2cf09e 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerContext.cs
@@ -30,7 +30,7 @@ public JsonSerializerOptions Options
if (_options == null)
{
_options = new JsonSerializerOptions();
- _options._context = this;
+ _options._serializerContext = this;
}
return _options;
@@ -97,13 +97,13 @@ protected JsonSerializerContext(JsonSerializerOptions? options)
{
if (options != null)
{
- if (options._context != null)
+ if (options._serializerContext != null)
{
ThrowHelper.ThrowInvalidOperationException_JsonSerializerOptionsAlreadyBoundToContext();
}
_options = options;
- options._context = this;
+ options._serializerContext = this;
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs
new file mode 100644
index 00000000000000..833ebb910fb487
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Caching.cs
@@ -0,0 +1,345 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace System.Text.Json
+{
+ public sealed partial class JsonSerializerOptions
+ {
+ ///
+ /// Encapsulates all cached metadata referenced by the current instance.
+ /// Context can be shared across multiple equivalent options instances.
+ ///
+ private CachingContext? _cachingContext;
+
+ // Simple LRU cache for the public (de)serialize entry points that avoid some lookups in _cachingContext.
+ // Although this may be written by multiple threads, 'volatile' was not added since any local affinity is fine.
+ private JsonTypeInfo? _lastTypeInfo;
+
+ internal JsonTypeInfo GetOrAddJsonTypeInfo(Type type)
+ {
+ if (_cachingContext == null)
+ {
+ InitializeCachingContext();
+ Debug.Assert(_cachingContext != null);
+ }
+
+ return _cachingContext.GetOrAddJsonTypeInfo(type);
+ }
+
+ internal bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo)
+ {
+ if (_cachingContext == null)
+ {
+ typeInfo = null;
+ return false;
+ }
+
+ return _cachingContext.TryGetJsonTypeInfo(type, out typeInfo);
+ }
+
+ internal bool IsJsonTypeInfoCached(Type type) => _cachingContext?.IsJsonTypeInfoCached(type) == true;
+
+ ///
+ /// Return the TypeInfo for root API calls.
+ /// This has an LRU cache that is intended only for public API calls that specify the root type.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal JsonTypeInfo GetOrAddJsonTypeInfoForRootType(Type type)
+ {
+ JsonTypeInfo? jsonTypeInfo = _lastTypeInfo;
+
+ if (jsonTypeInfo?.Type != type)
+ {
+ jsonTypeInfo = GetOrAddJsonTypeInfo(type);
+ _lastTypeInfo = jsonTypeInfo;
+ }
+
+ return jsonTypeInfo;
+ }
+
+ internal void ClearCaches()
+ {
+ _cachingContext?.Clear();
+ _lastTypeInfo = null;
+ }
+
+ private void InitializeCachingContext()
+ {
+ _cachingContext = TrackedCachingContexts.GetOrCreate(this);
+ }
+
+ ///
+ /// Stores and manages all reflection caches for one or more instances.
+ /// NB the type encapsulates the original options instance and only consults that one when building new types;
+ /// this is to prevent multiple options instances from leaking into the object graphs of converters which
+ /// could break user invariants.
+ ///
+ internal sealed class CachingContext
+ {
+ private readonly ConcurrentDictionary _converterCache = new();
+ private readonly ConcurrentDictionary _jsonTypeInfoCache = new();
+
+ public CachingContext(JsonSerializerOptions options)
+ {
+ Options = options;
+ _ = Count;
+ }
+
+ public JsonSerializerOptions Options { get; }
+ public int Count => _converterCache.Count + _jsonTypeInfoCache.Count;
+ public JsonConverter GetOrAddConverter(Type type) => _converterCache.GetOrAdd(type, Options.GetConverterFromType);
+ public JsonTypeInfo GetOrAddJsonTypeInfo(Type type) => _jsonTypeInfoCache.GetOrAdd(type, Options.GetJsonTypeInfoFromContextOrCreate);
+ public bool TryGetJsonTypeInfo(Type type, [NotNullWhen(true)] out JsonTypeInfo? typeInfo) => _jsonTypeInfoCache.TryGetValue(type, out typeInfo);
+ public bool IsJsonTypeInfoCached(Type type) => _jsonTypeInfoCache.ContainsKey(type);
+
+ public void Clear()
+ {
+ _converterCache.Clear();
+ _jsonTypeInfoCache.Clear();
+ }
+ }
+
+ ///
+ /// Defines a cache of CachingContexts; instead of using a ConditionalWeakTable which can be slow to traverse
+ /// this approach uses a regular dictionary pointing to weak references of .
+ /// Relevant caching contexts are looked up using the equality comparison defined by .
+ ///
+ internal static class TrackedCachingContexts
+ {
+ private const int MaxTrackedContexts = 64;
+ private static readonly ConcurrentDictionary> s_cache =
+ new(concurrencyLevel: 1, capacity: MaxTrackedContexts, new EqualityComparer());
+
+ private const int EvictionCountHistory = 16;
+ private static Queue s_recentEvictionCounts = new(EvictionCountHistory);
+ private static int s_evictionRunsToSkip;
+
+ public static CachingContext GetOrCreate(JsonSerializerOptions options)
+ {
+ ConcurrentDictionary> cache = s_cache;
+
+ if (cache.TryGetValue(options, out WeakReference? wr) && wr.TryGetTarget(out CachingContext? ctx))
+ {
+ return ctx;
+ }
+
+ lock (cache)
+ {
+ if (cache.TryGetValue(options, out wr))
+ {
+ if (!wr.TryGetTarget(out ctx))
+ {
+ // Found a dangling weak reference; replenish with a fresh instance.
+ ctx = new CachingContext(options);
+ wr.SetTarget(ctx);
+ }
+
+ return ctx;
+ }
+
+ if (cache.Count == MaxTrackedContexts)
+ {
+ if (!TryEvictDanglingEntries())
+ {
+ // Cache is full; return a fresh instance.
+ return new CachingContext(options);
+ }
+ }
+
+ Debug.Assert(cache.Count < MaxTrackedContexts);
+
+ // 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 };
+ Debug.Assert(key._cachingContext == null);
+
+ ctx = new CachingContext(options);
+ bool success = cache.TryAdd(key, new(ctx));
+ Debug.Assert(success);
+
+ return ctx;
+ }
+ }
+
+ public static void Clear()
+ {
+ lock (s_cache)
+ {
+ s_cache.Clear();
+ s_recentEvictionCounts.Clear();
+ s_evictionRunsToSkip = 0;
+ }
+ }
+
+ private static bool TryEvictDanglingEntries()
+ {
+ // Worst case scenario, the cache has been filled with permanent entries.
+ // Evictions are synchronized and each run is in the order of microseconds,
+ // so we want to avoid triggering runs every time an instance is initialized,
+ // For this reason we use a backoff strategy to average out the cost of eviction
+ // across multiple initializations. The backoff count is determined by the eviction
+ // rates of the most recent runs.
+
+ if (s_evictionRunsToSkip > 0)
+ {
+ --s_evictionRunsToSkip;
+ return false;
+ }
+
+ int currentEvictions = 0;
+ foreach (KeyValuePair> kvp in s_cache)
+ {
+ if (!kvp.Value.TryGetTarget(out _))
+ {
+ bool result = s_cache.TryRemove(kvp.Key, out _);
+ Debug.Assert(result);
+ currentEvictions++;
+ }
+ }
+
+ s_evictionRunsToSkip = EstimateEvictionRunsToSkip(currentEvictions);
+ return currentEvictions > 0;
+
+ // Estimate the number of eviction runs to skip based on recent eviction rates.
+ static int EstimateEvictionRunsToSkip(int latestEvictionCount)
+ {
+ Queue recentEvictionCounts = s_recentEvictionCounts;
+
+ if (recentEvictionCounts.Count < EvictionCountHistory - 1)
+ {
+ // Insufficient data points to determine a skip count.
+ recentEvictionCounts.Enqueue(latestEvictionCount);
+ return 0;
+ }
+ else if (recentEvictionCounts.Count == EvictionCountHistory)
+ {
+ recentEvictionCounts.Dequeue();
+ }
+
+ recentEvictionCounts.Enqueue(latestEvictionCount);
+
+ // Calculate the total number of eviction in the latest runs
+ // - If we have at least one eviction per run, on average,
+ // do not skip any future eviction runs.
+ // - Otherwise, skip ~the number of runs needed per one eviction.
+
+ int totalEvictions = 0;
+ foreach (int evictionCount in recentEvictionCounts)
+ {
+ totalEvictions += evictionCount;
+ }
+
+ int evictionRunsToSkip =
+ totalEvictions >= EvictionCountHistory ? 0 :
+ (int)Math.Round((double)EvictionCountHistory / Math.Max(totalEvictions, 1));
+
+ Debug.Assert(0 <= evictionRunsToSkip && evictionRunsToSkip <= EvictionCountHistory);
+ return evictionRunsToSkip;
+ }
+ }
+ }
+
+ ///
+ /// Provides a conservative equality comparison for JsonSerializerOptions instances.
+ /// If two instances are equivalent, they should generate identical metadata caches;
+ /// the converse however does not necessarily hold.
+ ///
+ private class EqualityComparer : IEqualityComparer
+ {
+ public bool Equals(JsonSerializerOptions? left, JsonSerializerOptions? right)
+ {
+ Debug.Assert(left != null && right != null);
+ return
+ left._dictionaryKeyPolicy == right._dictionaryKeyPolicy &&
+ left._jsonPropertyNamingPolicy == right._jsonPropertyNamingPolicy &&
+ left._readCommentHandling == right._readCommentHandling &&
+ left._referenceHandler == right._referenceHandler &&
+ left._encoder == right._encoder &&
+ left._defaultIgnoreCondition == right._defaultIgnoreCondition &&
+ left._numberHandling == right._numberHandling &&
+ left._unknownTypeHandling == right._unknownTypeHandling &&
+ left._defaultBufferSize == right._defaultBufferSize &&
+ left._maxDepth == right._maxDepth &&
+ left._allowTrailingCommas == right._allowTrailingCommas &&
+ left._ignoreNullValues == right._ignoreNullValues &&
+ left._ignoreReadOnlyProperties == right._ignoreReadOnlyProperties &&
+ left._ignoreReadonlyFields == right._ignoreReadonlyFields &&
+ left._includeFields == right._includeFields &&
+ left._propertyNameCaseInsensitive == right._propertyNameCaseInsensitive &&
+ left._writeIndented == right._writeIndented &&
+ left._serializerContext == right._serializerContext &&
+ CompareConverters(left.Converters, right.Converters);
+
+ static bool CompareConverters(IList left, IList right)
+ {
+ int n;
+ if ((n = left.Count) != right.Count)
+ {
+ return false;
+ }
+
+ for (int i = 0; i < n; i++)
+ {
+ if (left[i] != right[i])
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public int GetHashCode(JsonSerializerOptions options)
+ {
+ HashCode hc = default;
+
+ hc.Add(options._dictionaryKeyPolicy);
+ hc.Add(options._jsonPropertyNamingPolicy);
+ hc.Add(options._readCommentHandling);
+ hc.Add(options._referenceHandler);
+ hc.Add(options._encoder);
+ hc.Add(options._defaultIgnoreCondition);
+ hc.Add(options._numberHandling);
+ hc.Add(options._unknownTypeHandling);
+ hc.Add(options._defaultBufferSize);
+ hc.Add(options._maxDepth);
+ hc.Add(options._allowTrailingCommas);
+ hc.Add(options._ignoreNullValues);
+ hc.Add(options._ignoreReadOnlyProperties);
+ hc.Add(options._ignoreReadonlyFields);
+ hc.Add(options._includeFields);
+ hc.Add(options._propertyNameCaseInsensitive);
+ hc.Add(options._writeIndented);
+ hc.Add(options._serializerContext);
+
+ foreach (JsonConverter converter in options.Converters)
+ {
+ hc.Add(converter);
+ }
+
+ return hc.ToHashCode();
+ }
+
+#if !NETCOREAPP
+ ///
+ /// Polyfill for System.HashCode.
+ ///
+ private struct HashCode
+ {
+ private int _hashCode;
+ public void Add(T? value) => _hashCode = (_hashCode, value).GetHashCode();
+ public int ToHashCode() => _hashCode;
+ }
+#endif
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
index 4276d374058934..fda10abb0c0752 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -24,11 +23,8 @@ public sealed partial class JsonSerializerOptions
// The global list of built-in converters that override CanConvert().
private static JsonConverter[]? s_defaultFactoryConverters;
- // The cached converters (custom or built-in).
- private readonly ConcurrentDictionary _converters = new ConcurrentDictionary();
-
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
- private void RootBuiltInConverters()
+ private static void RootBuiltInConverters()
{
s_defaultSimpleConverters = GetDefaultSimpleConverters();
s_defaultFactoryConverters = new JsonConverter[]
@@ -97,7 +93,7 @@ void Add(JsonConverter converter) =>
///
public IList Converters { get; }
- internal JsonConverter DetermineConverter(Type? parentClassType, Type runtimePropertyType, MemberInfo? memberInfo)
+ internal JsonConverter GetConverterFromMember(Type? parentClassType, Type runtimePropertyType, MemberInfo? memberInfo)
{
JsonConverter converter = null!;
@@ -174,16 +170,21 @@ public JsonConverter GetConverter(Type typeToConvert)
internal JsonConverter GetConverterInternal(Type typeToConvert)
{
- Debug.Assert(typeToConvert != null);
-
- if (_converters.TryGetValue(typeToConvert, out JsonConverter? converter))
+ // Only cache the value once (de)serialization has occurred since new converters can be added that may change the result.
+ if (_cachingContext != null)
{
- Debug.Assert(converter != null);
- return converter;
+ return _cachingContext.GetOrAddConverter(typeToConvert);
}
+ return GetConverterFromType(typeToConvert);
+ }
+
+ private JsonConverter GetConverterFromType(Type typeToConvert)
+ {
+ Debug.Assert(typeToConvert != null);
+
// Priority 1: If there is a JsonSerializerContext, fetch the converter from there.
- converter = _context?.GetTypeInfo(typeToConvert)?.PropertyInfoForTypeInfo?.ConverterBase;
+ JsonConverter? converter = _serializerContext?.GetTypeInfo(typeToConvert)?.PropertyInfoForTypeInfo?.ConverterBase;
// Priority 2: Attempt to get custom converter added at runtime.
// Currently there is not a way at runtime to override the [JsonConverter] when applied to a property.
@@ -259,15 +260,6 @@ internal JsonConverter GetConverterInternal(Type typeToConvert)
ThrowHelper.ThrowInvalidOperationException_SerializationConverterNotCompatible(converter.GetType(), typeToConvert);
}
- // Only cache the value once (de)serialization has occurred since new converters can be added that may change the result.
- if (_haveTypesBeenCreated)
- {
- // A null converter is allowed here and cached.
-
- // Ignore failure case here in multi-threaded cases since the cached item will be equivalent.
- _converters.TryAdd(typeToConvert, converter);
- }
-
return converter;
}
@@ -319,7 +311,7 @@ private JsonConverter GetConverterFromAttribute(JsonConverterAttribute converter
internal bool TryGetDefaultSimpleConverter(Type typeToConvert, [NotNullWhen(true)] out JsonConverter? converter)
{
- if (_context == null && // For consistency do not return any default converters for
+ if (_serializerContext == null && // For consistency do not return any default converters for
// options instances linked to a JsonSerializerContext,
// even if the default converters might have been rooted.
s_defaultSimpleConverters != null &&
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
index fb5495a44c31ba..34915078571813 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -32,17 +31,12 @@ public sealed partial class JsonSerializerOptions
/// so using fresh default instances every time one is needed can result in redundant recomputation of converters.
/// This property provides a shared instance that can be consumed by any number of components without necessitating any converter recomputation.
///
- public static JsonSerializerOptions Default { get; } = new JsonSerializerOptions { _haveTypesBeenCreated = true };
+ public static JsonSerializerOptions Default { get; } = CreateDefaultImmutableInstance();
- private readonly ConcurrentDictionary _classes = new ConcurrentDictionary();
+ internal JsonSerializerContext? _serializerContext;
- // Simple LRU cache for the public (de)serialize entry points that avoid some lookups in _classes.
- // Although this may be written by multiple threads, 'volatile' was not added since any local affinity is fine.
- private JsonTypeInfo? _lastClass { get; set; }
-
- internal JsonSerializerContext? _context;
-
- private Func? _typeInfoCreationFunc;
+ // Stores the JsonTypeInfo factory, which requires unreferenced code and must be rooted by the reflection-based serializer.
+ private static Func? s_typeInfoCreationFunc;
// For any new option added, adding it to the options copied in the copy constructor below must be considered.
@@ -59,7 +53,6 @@ public sealed partial class JsonSerializerOptions
private int _defaultBufferSize = BufferSizeDefault;
private int _maxDepth;
private bool _allowTrailingCommas;
- private bool _haveTypesBeenCreated;
private bool _ignoreNullValues;
private bool _ignoreReadOnlyProperties;
private bool _ignoreReadonlyFields;
@@ -157,13 +150,13 @@ public JsonSerializerOptions(JsonSerializerDefaults defaults) : this()
///
public void AddContext() where TContext : JsonSerializerContext, new()
{
- if (_context != null)
+ if (_serializerContext != null)
{
ThrowHelper.ThrowInvalidOperationException_JsonSerializerOptionsAlreadyBoundToContext();
}
TContext context = new();
- _context = context;
+ _serializerContext = context;
context._options = this;
}
@@ -568,10 +561,10 @@ internal MemberAccessor MemberAccessorStrategy
#if NETCOREAPP
// if dynamic code isn't supported, fallback to reflection
_memberAccessorStrategy = RuntimeFeature.IsDynamicCodeSupported ?
- new ReflectionEmitMemberAccessor() :
+ new ReflectionEmitCachingMemberAccessor() :
new ReflectionMemberAccessor();
#elif NETFRAMEWORK
- _memberAccessorStrategy = new ReflectionEmitMemberAccessor();
+ _memberAccessorStrategy = new ReflectionEmitCachingMemberAccessor();
#else
_memberAccessorStrategy = new ReflectionMemberAccessor();
#endif
@@ -584,93 +577,40 @@ internal MemberAccessor MemberAccessorStrategy
///
/// Whether needs to be called.
///
- internal bool IsInitializedForReflectionSerializer { get; set; }
+ internal static bool IsInitializedForReflectionSerializer { get; set; }
///
/// Initializes the converters for the reflection-based serializer.
/// must be checked before calling.
///
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
- internal void InitializeForReflectionSerializer()
+ internal static void InitializeForReflectionSerializer()
{
// For threading cases, the state that is set here can be overwritten.
RootBuiltInConverters();
- _typeInfoCreationFunc = CreateJsonTypeInfo;
+ s_typeInfoCreationFunc = CreateJsonTypeInfo;
IsInitializedForReflectionSerializer = true;
[RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions options) => new JsonTypeInfo(type, options);
}
- internal JsonTypeInfo GetOrAddClass(Type type)
- {
- _haveTypesBeenCreated = true;
-
- if (!TryGetClass(type, out JsonTypeInfo? result))
- {
- result = _classes.GetOrAdd(type, GetClassFromContextOrCreate(type));
- }
-
- return result;
- }
- internal JsonTypeInfo GetClassFromContextOrCreate(Type type)
+ private JsonTypeInfo GetJsonTypeInfoFromContextOrCreate(Type type)
{
- JsonTypeInfo? info = _context?.GetTypeInfo(type);
+ JsonTypeInfo? info = _serializerContext?.GetTypeInfo(type);
if (info != null)
{
return info;
}
- if (_typeInfoCreationFunc == null)
+ if (s_typeInfoCreationFunc == null)
{
ThrowHelper.ThrowNotSupportedException_NoMetadataForType(type);
return null!;
}
- return _typeInfoCreationFunc(type, this);
- }
-
- ///
- /// Return the TypeInfo for root API calls.
- /// This has a LRU cache that is intended only for public API calls that specify the root type.
- ///
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal JsonTypeInfo GetOrAddClassForRootType(Type type)
- {
- JsonTypeInfo? jsonTypeInfo = _lastClass;
- if (jsonTypeInfo?.Type != type)
- {
- jsonTypeInfo = GetOrAddClass(type);
- _lastClass = jsonTypeInfo;
- }
-
- return jsonTypeInfo;
- }
-
- internal bool TryGetClass(Type type, [NotNullWhen(true)] out JsonTypeInfo? jsonTypeInfo)
- {
- // todo: for performance and reduced instances, consider using the converters and JsonTypeInfo from s_defaultOptions by cloning (or reference directly if no changes).
- // https://github.com/dotnet/runtime/issues/32357
- if (!_classes.TryGetValue(type, out JsonTypeInfo? result))
- {
- jsonTypeInfo = null;
- return false;
- }
-
- jsonTypeInfo = result;
- return true;
- }
-
- internal bool TypeIsCached(Type type)
- {
- return _classes.ContainsKey(type);
- }
-
- internal void ClearClasses()
- {
- _classes.Clear();
- _lastClass = null;
+ return s_typeInfoCreationFunc(type, this);
}
internal JsonDocumentOptions GetDocumentOptions()
@@ -716,10 +656,17 @@ internal JsonWriterOptions GetWriterOptions()
internal void VerifyMutable()
{
- if (_haveTypesBeenCreated || _context != null)
+ if (_cachingContext != null || _serializerContext != null)
{
- ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(_context);
+ ThrowHelper.ThrowInvalidOperationException_SerializerOptionsImmutable(_serializerContext);
}
}
+
+ private static JsonSerializerOptions CreateDefaultImmutableInstance()
+ {
+ var options = new JsonSerializerOptions();
+ options.InitializeCachingContext(); // eagerly initialize caching context to close type for modification.
+ return options;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs
index b79e417ec10d4b..4fd3c66edae085 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptionsUpdateHandler.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
[assembly: MetadataUpdateHandler(typeof(JsonSerializerOptionsUpdateHandler))]
@@ -17,8 +18,14 @@ public static void ClearCache(Type[]? types)
// Ignore the types, and just clear out all reflection caches from serializer options.
foreach (KeyValuePair options in JsonSerializerOptions.TrackedOptionsInstances.All)
{
- options.Key.ClearClasses();
+ options.Key.ClearCaches();
}
+
+ // Flush the shared caching contexts
+ JsonSerializerOptions.TrackedCachingContexts.Clear();
+
+ // Flush the dynamic method cache
+ ReflectionEmitCachingMemberAccessor.Clear();
}
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs
index 9139a027c5ea4f..aea214b684969b 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs
@@ -41,7 +41,7 @@ public JsonTypeInfo RuntimeTypeInfo
if (_runtimeTypeInfo == null)
{
Debug.Assert(Options != null);
- _runtimeTypeInfo = Options!.GetOrAddClass(RuntimePropertyType);
+ _runtimeTypeInfo = Options!.GetOrAddJsonTypeInfo(RuntimePropertyType);
}
return _runtimeTypeInfo;
@@ -102,7 +102,7 @@ public static JsonParameterInfo CreateIgnoredParameterPlaceholder(
Type parameterType = parameterInfo.ParameterType;
DefaultValueHolder holder;
- if (matchingProperty.Options.TryGetClass(parameterType, out JsonTypeInfo? typeInfo))
+ if (matchingProperty.Options.TryGetJsonTypeInfo(parameterType, out JsonTypeInfo? typeInfo))
{
holder = typeInfo.DefaultValueHolder;
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
index 5e2258ec6dde83..03d0e4fe6b84e5 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs
@@ -473,7 +473,7 @@ internal JsonTypeInfo RuntimeTypeInfo
{
if (_runtimeTypeInfo == null)
{
- _runtimeTypeInfo = Options.GetOrAddClass(RuntimePropertyType!);
+ _runtimeTypeInfo = Options.GetOrAddJsonTypeInfo(RuntimePropertyType!);
}
return _runtimeTypeInfo;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs
index f696cefe8f21cd..7cea26d207210f 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.Cache.cs
@@ -580,7 +580,7 @@ internal void InitializePropCache()
Debug.Assert(PropertyCache == null);
Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object);
- JsonSerializerContext? context = Options._context;
+ JsonSerializerContext? context = Options._serializerContext;
Debug.Assert(context != null);
JsonPropertyInfo[] array;
@@ -636,7 +636,7 @@ internal void InitializeParameterCache()
Debug.Assert(PropertyCache != null);
Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Object);
- JsonSerializerContext? context = Options._context;
+ JsonSerializerContext? context = Options._serializerContext;
Debug.Assert(context != null);
JsonParameterInfoValues[] array;
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
index 547173ba0f39e3..c309c3bcd8a0bf 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs
@@ -52,7 +52,7 @@ internal JsonTypeInfo? ElementTypeInfo
{
if (_elementTypeInfo == null && ElementType != null)
{
- _elementTypeInfo = Options.GetOrAddClass(ElementType);
+ _elementTypeInfo = Options.GetOrAddJsonTypeInfo(ElementType);
}
return _elementTypeInfo;
@@ -85,7 +85,7 @@ internal JsonTypeInfo? KeyTypeInfo
{
Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Dictionary);
- _keyTypeInfo = Options.GetOrAddClass(KeyType);
+ _keyTypeInfo = Options.GetOrAddJsonTypeInfo(KeyType);
}
return _keyTypeInfo;
@@ -600,7 +600,7 @@ private static JsonConverter GetConverter(
Debug.Assert(type != null);
ValidateType(type, parentClassType, memberInfo, options);
- JsonConverter converter = options.DetermineConverter(parentClassType, type, memberInfo);
+ JsonConverter converter = options.GetConverterFromMember(parentClassType, type, memberInfo);
// The runtimeType is the actual value being assigned to the property.
// There are three types to consider for the runtimeType:
@@ -649,7 +649,7 @@ private static JsonConverter GetConverter(
private static void ValidateType(Type type, Type? parentClassType, MemberInfo? memberInfo, JsonSerializerOptions options)
{
- if (!options.TypeIsCached(type) && IsInvalidForSerialization(type))
+ if (!options.IsJsonTypeInfoCached(type) && IsInvalidForSerialization(type))
{
ThrowHelper.ThrowInvalidOperationException_CannotSerializeInvalidType(type, parentClassType, memberInfo);
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.Cache.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.Cache.cs
new file mode 100644
index 00000000000000..c452977da0dcca
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.Cache.cs
@@ -0,0 +1,89 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NETFRAMEWORK || NETCOREAPP
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+
+namespace System.Text.Json.Serialization.Metadata
+{
+ internal sealed partial class ReflectionEmitCachingMemberAccessor
+ {
+ private sealed class Cache where TKey : notnull
+ {
+ private int _evictLock;
+ private long _lastEvictedTicks; // timestamp of latest eviction operation.
+ private readonly long _evictionIntervalTicks; // min timespan needed to trigger a new evict operation.
+ private readonly long _slidingExpirationTicks; // max timespan allowed for cache entries to remain inactive.
+ private readonly ConcurrentDictionary _cache = new();
+
+ public Cache(TimeSpan slidingExpiration, TimeSpan evictionInterval)
+ {
+ _slidingExpirationTicks = slidingExpiration.Ticks;
+ _evictionIntervalTicks = evictionInterval.Ticks;
+ _lastEvictedTicks = DateTime.UtcNow.Ticks;
+ }
+
+ public TValue GetOrAdd(TKey key, Func valueFactory) where TValue : class?
+ {
+ CacheEntry entry = _cache.GetOrAdd(
+ key,
+#if NETCOREAPP
+ static (TKey key, Func valueFactory) => new(valueFactory(key)),
+ valueFactory);
+#else
+ key => new(valueFactory(key)));
+#endif
+ long utcNowTicks = DateTime.UtcNow.Ticks;
+ Volatile.Write(ref entry.LastUsedTicks, utcNowTicks);
+
+ if (utcNowTicks - Volatile.Read(ref _lastEvictedTicks) >= _evictionIntervalTicks)
+ {
+ if (Interlocked.CompareExchange(ref _evictLock, 1, 0) == 0)
+ {
+ if (utcNowTicks - _lastEvictedTicks >= _evictionIntervalTicks)
+ {
+ EvictStaleCacheEntries(utcNowTicks);
+ Volatile.Write(ref _lastEvictedTicks, utcNowTicks);
+ }
+
+ Volatile.Write(ref _evictLock, 0);
+ }
+ }
+
+ return (TValue)entry.Value!;
+ }
+
+ public void Clear()
+ {
+ _cache.Clear();
+ _lastEvictedTicks = DateTime.UtcNow.Ticks;
+ }
+
+ private void EvictStaleCacheEntries(long utcNowTicks)
+ {
+ foreach (KeyValuePair kvp in _cache)
+ {
+ if (utcNowTicks - Volatile.Read(ref kvp.Value.LastUsedTicks) >= _slidingExpirationTicks)
+ {
+ _cache.TryRemove(kvp.Key, out _);
+ }
+ }
+ }
+
+ private class CacheEntry
+ {
+ public readonly object? Value;
+ public long LastUsedTicks;
+
+ public CacheEntry(object? value)
+ {
+ Value = value;
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs
new file mode 100644
index 00000000000000..61f9a8a3c9c02a
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/ReflectionEmitCachingMemberAccessor.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if NETFRAMEWORK || NETCOREAPP
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace System.Text.Json.Serialization.Metadata
+{
+ internal sealed partial class ReflectionEmitCachingMemberAccessor : MemberAccessor
+ {
+ private static readonly ReflectionEmitMemberAccessor s_sourceAccessor = new();
+ private static readonly Cache<(string id, Type declaringType, MemberInfo? member)> s_cache =
+ new(slidingExpiration: TimeSpan.FromMilliseconds(1000), evictionInterval: TimeSpan.FromMilliseconds(200));
+
+ public static void Clear() => s_cache.Clear();
+
+ public override Action CreateAddMethodDelegate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TCollection>()
+ => s_cache.GetOrAdd((nameof(CreateAddMethodDelegate), typeof(TCollection), null),
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2091:UnrecognizedReflectionPattern",
+ Justification = "Parent method annotation does not flow to lambda method, cf. https://github.com/dotnet/roslyn/issues/46646")]
+ static (_) => s_sourceAccessor.CreateAddMethodDelegate());
+
+ public override JsonTypeInfo.ConstructorDelegate? CreateConstructor([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type classType)
+ => s_cache.GetOrAdd((nameof(CreateConstructor), classType, null),
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2077:UnrecognizedReflectionPattern",
+ Justification = "Cannot apply DynamicallyAccessedMembersAttribute to tuple properties.")]
+ static (key) => s_sourceAccessor.CreateConstructor(key.declaringType));
+
+ public override Func