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 0fb5fcd5acf44f..a38e38ceb6a93c 100644
--- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj
+++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj
@@ -158,6 +158,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
+
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
index 2b189272f5d1e5..25bbf86537d72d 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
@@ -5,6 +5,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
@@ -212,6 +213,41 @@ public static string Utf8GetString(ReadOnlySpan bytes)
#endif
}
+ public static bool TryLookupUtf8Key(
+ this Dictionary dictionary,
+ ReadOnlySpan utf8Key,
+ [MaybeNullWhen(false)] out TValue result)
+ {
+#if NET9_0_OR_GREATER
+ Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer, string>);
+
+ Dictionary.AlternateLookup> spanLookup =
+ dictionary.GetAlternateLookup>();
+
+ char[]? rentedBuffer = null;
+
+ Span charBuffer = utf8Key.Length <= JsonConstants.StackallocCharThreshold ?
+ stackalloc char[JsonConstants.StackallocCharThreshold] :
+ (rentedBuffer = ArrayPool.Shared.Rent(utf8Key.Length));
+
+ int charsWritten = Encoding.UTF8.GetChars(utf8Key, charBuffer);
+ Span decodedKey = charBuffer[0..charsWritten];
+
+ bool success = spanLookup.TryGetValue(decodedKey, out result);
+
+ if (rentedBuffer != null)
+ {
+ decodedKey.Clear();
+ ArrayPool.Shared.Return(rentedBuffer);
+ }
+
+ return success;
+#else
+ string key = Utf8GetString(utf8Key);
+ return dictionary.TryGetValue(key, out result);
+#endif
+ }
+
///
/// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard.
///
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs
index c235fa4533937d..ee374daf4ec2d5 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectDefaultConverter.cs
@@ -171,6 +171,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
// Read method would have thrown if otherwise.
Debug.Assert(tokenType == JsonTokenType.PropertyName);
+ jsonTypeInfo.ValidateCanBeUsedForPropertyMetadataSerialization();
ReadOnlySpan unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty);
if (isAlreadyReadMetadataProperty)
{
@@ -185,7 +186,6 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
unescapedPropertyName,
ref state,
options,
- out byte[] _,
out bool useExtensionProperty);
state.Current.UseExtensionProperty = useExtensionProperty;
@@ -257,10 +257,10 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert,
Debug.Assert(obj != null);
value = (T)obj;
- // Check if we are trying to build the sorted cache.
- if (state.Current.PropertyRefCache != null)
+ // Check if we are trying to update the UTF-8 property cache.
+ if (state.Current.PropertyRefCacheBuilder != null)
{
- jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
+ jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current);
}
return true;
@@ -292,12 +292,12 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp
ReadOnlySpan unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty);
Debug.Assert(!isAlreadyReadMetadataProperty, "Only possible for types that can read metadata, which do not call into the fast-path method.");
+ jsonTypeInfo.ValidateCanBeUsedForPropertyMetadataSerialization();
JsonPropertyInfo jsonPropertyInfo = JsonSerializer.LookupProperty(
obj,
unescapedPropertyName,
ref state,
options,
- out byte[] _,
out bool useExtensionProperty);
ReadPropertyValue(obj, ref state, ref reader, jsonPropertyInfo, useExtensionProperty);
@@ -306,10 +306,10 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp
jsonTypeInfo.OnDeserialized?.Invoke(obj);
state.Current.ValidateAllRequiredPropertiesAreRead(jsonTypeInfo);
- // Check if we are trying to build the sorted cache.
- if (state.Current.PropertyRefCache != null)
+ // Check if we are trying to update the UTF-8 property cache.
+ if (state.Current.PropertyRefCacheBuilder != null)
{
- jsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
+ jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current);
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs
index d0d357903fc4ab..6ae225b57457fb 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectWithParameterizedConstructorConverter.cs
@@ -255,10 +255,10 @@ internal sealed override bool OnTryRead(ref Utf8JsonReader reader, Type typeToCo
Debug.Assert(obj != null);
value = (T)obj;
- // Check if we are trying to build the sorted cache.
- if (state.Current.PropertyRefCache != null)
+ // Check if we are trying to update the UTF-8 property cache.
+ if (state.Current.PropertyRefCacheBuilder != null)
{
- state.Current.JsonTypeInfo.UpdateSortedPropertyCache(ref state.Current);
+ jsonTypeInfo.UpdateUtf8PropertyCache(ref state.Current);
}
return true;
@@ -603,13 +603,9 @@ protected static bool TryLookupConstructorParameter(
unescapedPropertyName,
ref state,
options,
- out byte[] utf8PropertyName,
out bool useExtensionProperty,
createExtensionProperty: false);
- // For case insensitive and missing property support of JsonPath, remember the value on the temporary stack.
- state.Current.JsonPropertyName = utf8PropertyName;
-
jsonParameterInfo = jsonPropertyInfo.AssociatedParameter;
if (jsonParameterInfo != null)
{
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
index cf26e662c3370a..64a9de11e893a7 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializer.Read.HandlePropertyName.cs
@@ -21,17 +21,16 @@ internal static JsonPropertyInfo LookupProperty(
ReadOnlySpan unescapedPropertyName,
ref ReadStack state,
JsonSerializerOptions options,
- out byte[] utf8PropertyName,
out bool useExtensionProperty,
bool createExtensionProperty = true)
{
JsonTypeInfo jsonTypeInfo = state.Current.JsonTypeInfo;
useExtensionProperty = false;
- JsonPropertyInfo jsonPropertyInfo = jsonTypeInfo.GetProperty(
+ JsonPropertyInfo? jsonPropertyInfo = jsonTypeInfo.GetProperty(
unescapedPropertyName,
ref state.Current,
- out utf8PropertyName);
+ out byte[] utf8PropertyName);
// Increment PropertyIndex so GetProperty() checks the next property first when called again.
state.Current.PropertyIndex++;
@@ -40,7 +39,7 @@ internal static JsonPropertyInfo LookupProperty(
state.Current.JsonPropertyName = utf8PropertyName;
// Handle missing properties
- if (jsonPropertyInfo == JsonPropertyInfo.s_missingProperty)
+ if (jsonPropertyInfo is null)
{
if (jsonTypeInfo.EffectiveUnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow)
{
@@ -63,6 +62,11 @@ internal static JsonPropertyInfo LookupProperty(
jsonPropertyInfo = dataExtProperty;
useExtensionProperty = true;
}
+ else
+ {
+ // Populate with a placeholder value required by JsonPath calculations
+ jsonPropertyInfo = JsonPropertyInfo.s_missingProperty;
+ }
}
state.Current.JsonPropertyInfo = jsonPropertyInfo;
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 0adea37c3c6f40..4077c89fe97894 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
@@ -800,12 +800,12 @@ public string Name
///
/// Utf8 version of Name.
///
- internal byte[] NameAsUtf8Bytes { get; set; } = null!;
+ internal byte[] NameAsUtf8Bytes { get; private set; } = null!;
///
/// The escaped name passed to the writer.
///
- internal byte[] EscapedNameSection { get; set; } = null!;
+ internal byte[] EscapedNameSection { get; private set; } = null!;
///
/// Gets the value associated with the current contract instance.
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 ef778611fbd70d..13f0821f8b32d5 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
@@ -3,10 +3,7 @@
using System.Collections.Generic;
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Text.Json.Reflection;
namespace System.Text.Json.Serialization.Metadata
{
@@ -17,14 +14,6 @@ public abstract partial class JsonTypeInfo
///
internal static readonly Type ObjectType = typeof(object);
- // The length of the property name embedded in the key (in bytes).
- // The key is a ulong (8 bytes) containing the first 7 bytes of the property name
- // followed by a byte representing the length.
- private const int PropertyNameKeyLength = 7;
-
- // The limit to how many property names from the JSON are cached in _propertyRefsSorted before using PropertyCache.
- private const int PropertyNameCountCacheThreshold = 64;
-
// The number of parameters the deserialization constructor has. If this is not equal to ParameterCache.Count, this means
// that not all parameters are bound to object properties, and an exception will be thrown if deserialization is attempted.
internal int ParameterCount { get; private protected set; }
@@ -74,64 +63,38 @@ internal Dictionary PropertyIndex
private Dictionary? _propertyIndex;
- // Fast cache of properties by first JSON ordering; may not contain all properties. Accessed before PropertyCache.
- // Use an array (instead of List) for highest performance.
- private volatile PropertyRef[]? _propertyRefsSorted;
-
- [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
- [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
- internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType, Type? declaringType)
- {
- JsonPropertyInfo jsonPropertyInfo;
-
- if (Options.TryGetTypeInfoCached(propertyType, out JsonTypeInfo? jsonTypeInfo))
- {
- // If a JsonTypeInfo has already been cached for the property type,
- // avoid reflection-based initialization by delegating construction
- // of JsonPropertyInfo construction to the property type metadata.
- jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, declaringType, Options);
- }
- else
- {
- // Metadata for `propertyType` has not been registered yet.
- // Use reflection to instantiate the correct JsonPropertyInfo
- Type propertyInfoType = typeof(JsonPropertyInfo<>).MakeGenericType(propertyType);
- jsonPropertyInfo = (JsonPropertyInfo)propertyInfoType.CreateInstanceNoWrapExceptions(
- parameterTypes: new Type[] { typeof(Type), typeof(JsonTypeInfo), typeof(JsonSerializerOptions) },
- parameters: new object[] { declaringType ?? Type, this, Options })!;
- }
-
- Debug.Assert(jsonPropertyInfo.PropertyType == propertyType);
- return jsonPropertyInfo;
- }
-
///
- /// Creates a JsonPropertyInfo whose property type matches the type of this JsonTypeInfo instance.
+ /// Stores a cache of UTF-8 encoded property names and their associated JsonPropertyInfo, if available.
+ /// Consulted before the lookup to avoid added allocations and decoding costs.
+ /// The cache is grown on-demand appending encountered unbounded properties or alternative casings.
///
- private protected abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo declaringTypeInfo, Type? declaringType, JsonSerializerOptions options);
+ private PropertyRef[] _utf8PropertyCache = [];
- // AggressiveInlining used although a large method it is only called from one location and is on a hot path.
+ ///
+ /// Defines the core property lookup logic for a given unescaped UTF-8 encoded property name.
+ ///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal JsonPropertyInfo GetProperty(
- ReadOnlySpan propertyName,
- ref ReadStackFrame frame,
- out byte[] utf8PropertyName)
+ internal JsonPropertyInfo? GetProperty(ReadOnlySpan propertyName, ref ReadStackFrame frame, out byte[] utf8PropertyName)
{
- PropertyRef propertyRef;
+ Debug.Assert(IsConfigured);
- ValidateCanBeUsedForPropertyMetadataSerialization();
- ulong key = GetKey(propertyName);
+ // The logic can be broken up into roughly three stages:
+ // 1. Look up the UTF-8 property cache for potential exact matches in the encoding.
+ // 2. If no match is found, decode to UTF-16 and look up the primary dictionary.
+ // 3. Store the new result for potential inclusion to the UTF-8 cache once deserialization is complete.
- // Keep a local copy of the cache in case it changes by another thread.
- PropertyRef[]? localPropertyRefsSorted = _propertyRefsSorted;
+ PropertyRef[] utf8PropertyCache = _utf8PropertyCache; // Keep a local copy of the cache in case it changes by another thread.
+ ReadOnlySpan utf8PropertyCacheSpan = utf8PropertyCache;
+ ulong key = PropertyRef.GetKey(propertyName);
- // If there is an existing cache, then use it.
- if (localPropertyRefsSorted != null)
+ if (!utf8PropertyCacheSpan.IsEmpty)
{
+ PropertyRef propertyRef;
+
// Start with the current property index, and then go forwards\backwards.
int propertyIndex = frame.PropertyIndex;
- int count = localPropertyRefsSorted.Length;
+ int count = utf8PropertyCacheSpan.Length;
int iForward = Math.Min(propertyIndex, count);
int iBackward = iForward - 1;
@@ -139,10 +102,10 @@ internal JsonPropertyInfo GetProperty(
{
if (iForward < count)
{
- propertyRef = localPropertyRefsSorted[iForward];
- if (IsPropertyRefEqual(propertyRef, propertyName, key))
+ propertyRef = utf8PropertyCacheSpan[iForward];
+ if (propertyRef.Equals(propertyName, key))
{
- utf8PropertyName = propertyRef.NameFromJson;
+ utf8PropertyName = propertyRef.Utf8PropertyName;
return propertyRef.Info;
}
@@ -150,10 +113,10 @@ internal JsonPropertyInfo GetProperty(
if (iBackward >= 0)
{
- propertyRef = localPropertyRefsSorted[iBackward];
- if (IsPropertyRefEqual(propertyRef, propertyName, key))
+ propertyRef = utf8PropertyCacheSpan[iBackward];
+ if (propertyRef.Equals(propertyName, key))
{
- utf8PropertyName = propertyRef.NameFromJson;
+ utf8PropertyName = propertyRef.Utf8PropertyName;
return propertyRef.Info;
}
@@ -162,10 +125,10 @@ internal JsonPropertyInfo GetProperty(
}
else if (iBackward >= 0)
{
- propertyRef = localPropertyRefsSorted[iBackward];
- if (IsPropertyRefEqual(propertyRef, propertyName, key))
+ propertyRef = utf8PropertyCacheSpan[iBackward];
+ if (propertyRef.Equals(propertyName, key))
{
- utf8PropertyName = propertyRef.NameFromJson;
+ utf8PropertyName = propertyRef.Utf8PropertyName;
return propertyRef.Info;
}
@@ -180,176 +143,49 @@ internal JsonPropertyInfo GetProperty(
}
// No cached item was found. Try the main dictionary which has all of the properties.
- if (PropertyIndex.TryGetValue(JsonHelpers.Utf8GetString(propertyName), out JsonPropertyInfo? info))
+ if (PropertyIndex.TryLookupUtf8Key(propertyName, out JsonPropertyInfo? info) &&
+ (!Options.PropertyNameCaseInsensitive || propertyName.SequenceEqual(info.NameAsUtf8Bytes)))
{
- Debug.Assert(info != null, "PropertyCache contains null JsonPropertyInfo");
-
- if (Options.PropertyNameCaseInsensitive)
- {
- if (propertyName.SequenceEqual(info.NameAsUtf8Bytes))
- {
- // Use the existing byte[] reference instead of creating another one.
- utf8PropertyName = info.NameAsUtf8Bytes!;
- }
- else
- {
- // Make a copy of the original Span.
- utf8PropertyName = propertyName.ToArray();
- }
- }
- else
- {
- utf8PropertyName = info.NameAsUtf8Bytes;
- }
+ // We have an exact match in UTF8 encoding.
+ utf8PropertyName = info.NameAsUtf8Bytes;
}
else
{
- info = JsonPropertyInfo.s_missingProperty;
-
// Make a copy of the original Span.
utf8PropertyName = propertyName.ToArray();
}
- // Check if we should add this to the cache.
- // Only cache up to a threshold length and then just use the dictionary when an item is not found in the cache.
- int cacheCount = 0;
- if (localPropertyRefsSorted != null)
- {
- cacheCount = localPropertyRefsSorted.Length;
- }
+ // Assuming there is capacity, store the new result for potential
+ // inclusion to the UTF-8 cache once deserialization is complete.
- // Do a quick check for the stable (after warm-up) case.
- if (cacheCount < PropertyNameCountCacheThreshold)
+ ref PropertyRefCacheBuilder? cacheBuilder = ref frame.PropertyRefCacheBuilder;
+ if ((cacheBuilder?.TotalCount ?? utf8PropertyCache.Length) < PropertyRefCacheBuilder.MaxCapacity)
{
- // Do a slower check for the warm-up case.
- if (frame.PropertyRefCache != null)
- {
- cacheCount += frame.PropertyRefCache.Count;
- }
-
- // Check again to append the cache up to the threshold.
- if (cacheCount < PropertyNameCountCacheThreshold)
- {
- frame.PropertyRefCache ??= new List();
-
- Debug.Assert(info != null);
-
- propertyRef = new PropertyRef(key, info, utf8PropertyName);
- frame.PropertyRefCache.Add(propertyRef);
- }
+ (cacheBuilder ??= new(utf8PropertyCache)).TryAdd(new(key, info, utf8PropertyName));
}
return info;
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static bool IsPropertyRefEqual(in PropertyRef propertyRef, ReadOnlySpan propertyName, ulong key)
- {
- if (key == propertyRef.Key)
- {
- // We compare the whole name, although we could skip the first 7 bytes (but it's not any faster)
- if (propertyName.Length <= PropertyNameKeyLength ||
- propertyName.SequenceEqual(propertyRef.NameFromJson))
- {
- return true;
- }
- }
-
- return false;
- }
-
///
- /// Get a key from the property name.
- /// The key consists of the first 7 bytes of the property name and then the length.
+ /// Attempts to update the UTF-8 property cache with the results gathered in the current deserialization operation.
+ /// The update operation is done optimistically and results are discarded if the cache was updated by another thread.
///
- // AggressiveInlining used since this method is only called from two locations and is on a hot path.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- internal static ulong GetKey(ReadOnlySpan name)
- {
- ulong key;
-
- ref byte reference = ref MemoryMarshal.GetReference(name);
- int length = name.Length;
-
- if (length > 7)
- {
- key = Unsafe.ReadUnaligned(ref reference) & 0x00ffffffffffffffL;
- key |= (ulong)Math.Min(length, 0xff) << 56;
- }
- else
- {
- key =
- length > 5 ? Unsafe.ReadUnaligned(ref reference) | (ulong)Unsafe.ReadUnaligned(ref Unsafe.Add(ref reference, 4)) << 32 :
- length > 3 ? Unsafe.ReadUnaligned(ref reference) :
- length > 1 ? Unsafe.ReadUnaligned(ref reference) : 0UL;
- key |= (ulong)length << 56;
-
- if ((length & 1) != 0)
- {
- var offset = length - 1;
- key |= (ulong)Unsafe.Add(ref reference, offset) << (offset * 8);
- }
- }
-
-#if DEBUG
- // Verify key contains the embedded bytes as expected.
- // Note: the expected properties do not hold true on big-endian platforms
- if (BitConverter.IsLittleEndian)
- {
- const int BitsInByte = 8;
- Debug.Assert(
- // Verify embedded property name.
- (name.Length < 1 || name[0] == ((key & ((ulong)0xFF << BitsInByte * 0)) >> BitsInByte * 0)) &&
- (name.Length < 2 || name[1] == ((key & ((ulong)0xFF << BitsInByte * 1)) >> BitsInByte * 1)) &&
- (name.Length < 3 || name[2] == ((key & ((ulong)0xFF << BitsInByte * 2)) >> BitsInByte * 2)) &&
- (name.Length < 4 || name[3] == ((key & ((ulong)0xFF << BitsInByte * 3)) >> BitsInByte * 3)) &&
- (name.Length < 5 || name[4] == ((key & ((ulong)0xFF << BitsInByte * 4)) >> BitsInByte * 4)) &&
- (name.Length < 6 || name[5] == ((key & ((ulong)0xFF << BitsInByte * 5)) >> BitsInByte * 5)) &&
- (name.Length < 7 || name[6] == ((key & ((ulong)0xFF << BitsInByte * 6)) >> BitsInByte * 6)) &&
- // Verify embedded length.
- (name.Length >= 0xFF || (key & ((ulong)0xFF << BitsInByte * 7)) >> BitsInByte * 7 == (ulong)name.Length) &&
- (name.Length < 0xFF || (key & ((ulong)0xFF << BitsInByte * 7)) >> BitsInByte * 7 == 0xFF),
- "Embedded bytes not as expected");
- }
-#endif
-
- return key;
- }
-
- internal void UpdateSortedPropertyCache(ref ReadStackFrame frame)
+ internal void UpdateUtf8PropertyCache(ref ReadStackFrame frame)
{
- Debug.Assert(frame.PropertyRefCache != null);
+ Debug.Assert(frame.PropertyRefCacheBuilder is { Count: > 0 });
- // frame.PropertyRefCache is only read\written by a single thread -- the thread performing
- // the deserialization for a given object instance.
+ PropertyRef[]? currentCache = _utf8PropertyCache;
+ PropertyRefCacheBuilder cacheBuilder = frame.PropertyRefCacheBuilder;
- List listToAppend = frame.PropertyRefCache;
-
- // _propertyRefsSorted can be accessed by multiple threads, so replace the reference when
- // appending to it. No lock() is necessary.
-
- if (_propertyRefsSorted != null)
- {
- List replacementList = new List(_propertyRefsSorted);
- Debug.Assert(replacementList.Count <= PropertyNameCountCacheThreshold);
-
- // Verify replacementList will not become too large.
- while (replacementList.Count + listToAppend.Count > PropertyNameCountCacheThreshold)
- {
- // This code path is rare; keep it simple by using RemoveAt() instead of RemoveRange() which requires calculating index\count.
- listToAppend.RemoveAt(listToAppend.Count - 1);
- }
-
- // Add the new items; duplicates are possible but that is tolerated during property lookup.
- replacementList.AddRange(listToAppend);
- _propertyRefsSorted = replacementList.ToArray();
- }
- else
+ if (currentCache == cacheBuilder.OriginalCache)
{
- _propertyRefsSorted = listToAppend.ToArray();
+ PropertyRef[] newCache = cacheBuilder.ToArray();
+ Debug.Assert(newCache.Length <= PropertyRefCacheBuilder.MaxCapacity);
+ _utf8PropertyCache = cacheBuilder.ToArray();
}
- frame.PropertyRefCache = null;
+ frame.PropertyRefCacheBuilder = null;
}
}
}
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 f337adcb567b0c..b647ea4384b1d6 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
@@ -966,7 +966,7 @@ internal static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonConverter convert
{
Type jsonTypeInfoType = typeof(JsonTypeInfo<>).MakeGenericType(type);
jsonTypeInfo = (JsonTypeInfo)jsonTypeInfoType.CreateInstanceNoWrapExceptions(
- parameterTypes: new Type[] { typeof(JsonConverter), typeof(JsonSerializerOptions) },
+ parameterTypes: [typeof(JsonConverter), typeof(JsonSerializerOptions)],
parameters: new object[] { converter, options })!;
}
@@ -1009,6 +1009,38 @@ public JsonPropertyInfo CreateJsonPropertyInfo(Type propertyType, string name)
return propertyInfo;
}
+ [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
+ [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)]
+ internal JsonPropertyInfo CreatePropertyUsingReflection(Type propertyType, Type? declaringType)
+ {
+ JsonPropertyInfo jsonPropertyInfo;
+
+ if (Options.TryGetTypeInfoCached(propertyType, out JsonTypeInfo? jsonTypeInfo))
+ {
+ // If a JsonTypeInfo has already been cached for the property type,
+ // avoid reflection-based initialization by delegating construction
+ // of JsonPropertyInfo construction to the property type metadata.
+ jsonPropertyInfo = jsonTypeInfo.CreateJsonPropertyInfo(declaringTypeInfo: this, declaringType, Options);
+ }
+ else
+ {
+ // Metadata for `propertyType` has not been registered yet.
+ // Use reflection to instantiate the correct JsonPropertyInfo
+ Type propertyInfoType = typeof(JsonPropertyInfo<>).MakeGenericType(propertyType);
+ jsonPropertyInfo = (JsonPropertyInfo)propertyInfoType.CreateInstanceNoWrapExceptions(
+ parameterTypes: [typeof(Type), typeof(JsonTypeInfo), typeof(JsonSerializerOptions)],
+ parameters: new object[] { declaringType ?? Type, this, Options })!;
+ }
+
+ Debug.Assert(jsonPropertyInfo.PropertyType == propertyType);
+ return jsonPropertyInfo;
+ }
+
+ ///
+ /// Creates a JsonPropertyInfo whose property type matches the type of this JsonTypeInfo instance.
+ ///
+ private protected abstract JsonPropertyInfo CreateJsonPropertyInfo(JsonTypeInfo declaringTypeInfo, Type? declaringType, JsonSerializerOptions options);
+
private protected Dictionary? _parameterInfoValuesIndex;
// Untyped, root-level serialization methods
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs
index a3e0a2aa4af58c..d1f4c0afaf8dc9 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRef.cs
@@ -1,21 +1,100 @@
// 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.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
namespace System.Text.Json.Serialization.Metadata
{
- internal readonly struct PropertyRef
+ ///
+ /// Represents a UTF-8 encoded JSON property name and its associated , if available.
+ /// PropertyRefs use byte sequence equality, so equal JSON strings with alternate encodings or casings are not equal.
+ /// Used as a first-level cache for property lookups before falling back to UTF decoding and string comparison.
+ ///
+ internal readonly struct PropertyRef(ulong key, JsonPropertyInfo? info, byte[] utf8PropertyName) : IEquatable
{
- public PropertyRef(ulong key, JsonPropertyInfo info, byte[] nameFromJson)
+ // The length of the property name embedded in the key (in bytes).
+ // The key is a ulong (8 bytes) containing the first 7 bytes of the property name
+ // followed by a byte representing the length.
+ private const int PropertyNameKeyLength = 7;
+
+ ///
+ /// A custom hashcode produced from the UTF-8 encoded property name.
+ ///
+ public readonly ulong Key = key;
+
+ ///
+ /// The associated with the property name, if available.
+ ///
+ public readonly JsonPropertyInfo? Info = info;
+
+ ///
+ /// Caches a heap allocated copy of the UTF-8 encoded property name.
+ ///
+ public readonly byte[] Utf8PropertyName = utf8PropertyName;
+
+ public bool Equals(PropertyRef other) => Equals(other.Utf8PropertyName, other.Key);
+ public override bool Equals(object? obj) => obj is PropertyRef other && Equals(other);
+ public override int GetHashCode() => Key.GetHashCode();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(ReadOnlySpan propertyName, ulong key)
{
- Key = key;
- Info = info;
- NameFromJson = nameFromJson;
+ // If the property name is less than 8 bytes, it is embedded in the key so no further comparison is necessary.
+ return key == Key && (propertyName.Length <= PropertyNameKeyLength || propertyName.SequenceEqual(Utf8PropertyName));
}
- public readonly ulong Key;
- public readonly JsonPropertyInfo Info;
+ ///
+ /// Get a key from the property name.
+ /// The key consists of the first 7 bytes of the property name and then the least significant bits of the length.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ulong GetKey(ReadOnlySpan name)
+ {
+ ref byte reference = ref MemoryMarshal.GetReference(name);
+ int length = name.Length;
+ ulong key = (ulong)(byte)length << 56;
+
+ switch (length)
+ {
+ case 0: goto ComputedKey;
+ case 1: goto OddLength;
+ case 2: key |= Unsafe.ReadUnaligned(ref reference); goto ComputedKey;
+ case 3: key |= Unsafe.ReadUnaligned(ref reference); goto OddLength;
+ case 4: key |= Unsafe.ReadUnaligned(ref reference); goto ComputedKey;
+ case 5: key |= Unsafe.ReadUnaligned(ref reference); goto OddLength;
+ case 6: key |= Unsafe.ReadUnaligned(ref reference) | (ulong)Unsafe.ReadUnaligned(ref Unsafe.Add(ref reference, 4)) << 32; goto ComputedKey;
+ case 7: key |= Unsafe.ReadUnaligned(ref reference) | (ulong)Unsafe.ReadUnaligned(ref Unsafe.Add(ref reference, 4)) << 32; goto OddLength;
+ default: key |= Unsafe.ReadUnaligned(ref reference) & 0x00ffffffffffffffL; goto ComputedKey;
+ }
- // NameFromJson may be different than Info.NameAsUtf8Bytes when case insensitive is enabled.
- public readonly byte[] NameFromJson;
+ OddLength:
+ int offset = length - 1;
+ key |= (ulong)Unsafe.Add(ref reference, offset) << (offset * 8);
+
+ ComputedKey:
+#if DEBUG
+ // Verify key contains the embedded bytes as expected.
+ // Note: the expected properties do not hold true on big-endian platforms
+ if (BitConverter.IsLittleEndian)
+ {
+ const int BitsInByte = 8;
+ Debug.Assert(
+ // Verify embedded property name.
+ (name.Length < 1 || name[0] == ((key & ((ulong)0xFF << BitsInByte * 0)) >> BitsInByte * 0)) &&
+ (name.Length < 2 || name[1] == ((key & ((ulong)0xFF << BitsInByte * 1)) >> BitsInByte * 1)) &&
+ (name.Length < 3 || name[2] == ((key & ((ulong)0xFF << BitsInByte * 2)) >> BitsInByte * 2)) &&
+ (name.Length < 4 || name[3] == ((key & ((ulong)0xFF << BitsInByte * 3)) >> BitsInByte * 3)) &&
+ (name.Length < 5 || name[4] == ((key & ((ulong)0xFF << BitsInByte * 4)) >> BitsInByte * 4)) &&
+ (name.Length < 6 || name[5] == ((key & ((ulong)0xFF << BitsInByte * 5)) >> BitsInByte * 5)) &&
+ (name.Length < 7 || name[6] == ((key & ((ulong)0xFF << BitsInByte * 6)) >> BitsInByte * 6)) &&
+ // Verify embedded length.
+ (key & ((ulong)0xFF << BitsInByte * 7)) >> BitsInByte * 7 == (byte)name.Length,
+ "Embedded bytes not as expected");
+ }
+#endif
+ return key;
+ }
}
}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs
new file mode 100644
index 00000000000000..7aacd45beb5975
--- /dev/null
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/PropertyRefCacheBuilder.cs
@@ -0,0 +1,36 @@
+// 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.Generic;
+using System.Diagnostics;
+
+namespace System.Text.Json.Serialization.Metadata
+{
+ ///
+ /// Defines builder type for constructing updated caches.
+ ///
+ internal sealed class PropertyRefCacheBuilder(PropertyRef[] originalCache)
+ {
+ public const int MaxCapacity = 64;
+ private readonly List _propertyRefs = [];
+ private readonly HashSet _added = [];
+
+ ///
+ /// Stores a reference to the original cache off which the current list is being built.
+ ///
+ public readonly PropertyRef[] OriginalCache = originalCache;
+ public int Count => _propertyRefs.Count;
+ public int TotalCount => OriginalCache.Length + _propertyRefs.Count;
+ public PropertyRef[] ToArray() => [.. OriginalCache, .. _propertyRefs];
+
+ public void TryAdd(PropertyRef propertyRef)
+ {
+ Debug.Assert(TotalCount < MaxCapacity, "Should have been checked by the caller.");
+
+ if (_added.Add(propertyRef))
+ {
+ _propertyRefs.Add(propertyRef);
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
index 065e03ed4c6e9d..0c973ea2b2350c 100644
--- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
+++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/ReadStackFrame.cs
@@ -65,7 +65,9 @@ public JsonTypeInfo BaseJsonTypeInfo
// For performance, we order the properties by the first deserialize and PropertyIndex helps find the right slot quicker.
public int PropertyIndex;
- public List? PropertyRefCache;
+
+ // Tracks newly encounentered UTF-8 encoded properties during the current deserialization, to be appended to the cache.
+ public PropertyRefCacheBuilder? PropertyRefCacheBuilder;
// Holds relevant state when deserializing objects with parameterized constructors.
public ArgumentState? CtorArgumentState;