Skip to content

Commit

Permalink
Clean up JSON property lookup logic and add alternate key lookup supp…
Browse files Browse the repository at this point in the history
…ort. (#103836)

* Clean up JSON property lookup logic and add alternate lookup support.

* Ensure PropertyRef cache doesn't contain duplicates.

* Remove usings.

* Revert back to using original caching algorithm.

* Incorporate suggestions to key generation algorithm.

* Address feedback.

* Simplify more PropertyRef methods.
  • Loading branch information
eiriktsarpalis committed Jul 8, 2024
1 parent f99194c commit e4d9e26
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 245 deletions.
1 change: 1 addition & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoKind.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoResolverChain.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoResolverWithAddedModifiers.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\PropertyRefCacheBuilder.cs" />
<Compile Include="System\Text\Json\Serialization\PolymorphicSerializationState.cs" />
<Compile Include="System\Text\Json\Writer\Utf8JsonWriterCache.cs" />
<Compile Include="System\Text\Json\Serialization\ReferenceEqualsWrapper.cs" />
Expand Down
36 changes: 36 additions & 0 deletions src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

Expand Down Expand Up @@ -213,6 +214,41 @@ public static string Utf8GetString(ReadOnlySpan<byte> bytes)
#endif
}

public static bool TryLookupUtf8Key<TValue>(
this Dictionary<string, TValue> dictionary,
ReadOnlySpan<byte> utf8Key,
[MaybeNullWhen(false)] out TValue result)
{
#if NET9_0_OR_GREATER
Debug.Assert(dictionary.Comparer is IAlternateEqualityComparer<ReadOnlySpan<char>, string>);

Dictionary<string, TValue>.AlternateLookup<ReadOnlySpan<char>> spanLookup =
dictionary.GetAlternateLookup<string, TValue, ReadOnlySpan<char>>();

char[]? rentedBuffer = null;

Span<char> charBuffer = utf8Key.Length <= JsonConstants.StackallocCharThreshold ?
stackalloc char[JsonConstants.StackallocCharThreshold] :
(rentedBuffer = ArrayPool<char>.Shared.Rent(utf8Key.Length));

int charsWritten = Encoding.UTF8.GetChars(utf8Key, charBuffer);
Span<char> decodedKey = charBuffer[0..charsWritten];

bool success = spanLookup.TryGetValue(decodedKey, out result);

if (rentedBuffer != null)
{
decodedKey.Clear();
ArrayPool<char>.Shared.Return(rentedBuffer);
}

return success;
#else
string key = Utf8GetString(utf8Key);
return dictionary.TryGetValue(key, out result);
#endif
}

/// <summary>
/// Emulates Dictionary(IEnumerable{KeyValuePair}) on netstandard.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> unescapedPropertyName = JsonSerializer.GetPropertyName(ref state, ref reader, options, out bool isAlreadyReadMetadataProperty);
if (isAlreadyReadMetadataProperty)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -292,12 +292,12 @@ internal static void PopulatePropertiesFastPath(object obj, JsonTypeInfo jsonTyp
ReadOnlySpan<byte> 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);
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@ internal static JsonPropertyInfo LookupProperty(
ReadOnlySpan<byte> 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++;
Expand All @@ -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)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,12 +800,12 @@ public string Name
/// <summary>
/// Utf8 version of Name.
/// </summary>
internal byte[] NameAsUtf8Bytes { get; set; } = null!;
internal byte[] NameAsUtf8Bytes { get; private set; } = null!;

/// <summary>
/// The escaped name passed to the writer.
/// </summary>
internal byte[] EscapedNameSection { get; set; } = null!;
internal byte[] EscapedNameSection { get; private set; } = null!;

/// <summary>
/// Gets the <see cref="JsonSerializerOptions"/> value associated with the current contract instance.
Expand Down
Loading

0 comments on commit e4d9e26

Please sign in to comment.