Skip to content

Commit 4d5d2dc

Browse files
authored
JSON: Add support for {ReadOnly}Memory<T> (#88713)
* Add support for {ReadOnly}Memory * Address feedback * Remove unintentional comment * Register {ReadOnly}MemoryByteType as a known built-in type
1 parent dedaf46 commit 4d5d2dc

23 files changed

+507
-28
lines changed

src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,18 @@ public KnownTypeSymbols(Compilation compilation)
141141

142142
private Option<IArrayTypeSymbol?> _ByteArrayType;
143143

144+
public INamedTypeSymbol? MemoryByteType => _MemoryByteType.HasValue
145+
? _MemoryByteType.Value
146+
: (_MemoryByteType = new(MemoryType?.Construct(Compilation.GetSpecialType(SpecialType.System_Byte)))).Value;
147+
148+
private Option<INamedTypeSymbol?> _MemoryByteType;
149+
150+
public INamedTypeSymbol? ReadOnlyMemoryByteType => _ReadOnlyMemoryByteType.HasValue
151+
? _ReadOnlyMemoryByteType.Value
152+
: (_ReadOnlyMemoryByteType = new(ReadOnlyMemoryType?.Construct(Compilation.GetSpecialType(SpecialType.System_Byte)))).Value;
153+
154+
private Option<INamedTypeSymbol?> _ReadOnlyMemoryByteType;
155+
144156
public INamedTypeSymbol? GuidType => GetOrResolveType(typeof(Guid), ref _GuidType);
145157
private Option<INamedTypeSymbol?> _GuidType;
146158

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ private SourceText GenerateForCollection(ContextGenerationSpec contextSpec, Type
337337
switch (collectionType)
338338
{
339339
case CollectionType.Array:
340+
case CollectionType.MemoryOfT:
341+
case CollectionType.ReadOnlyMemoryOfT:
340342
createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName})";
341343
break;
342344
case CollectionType.IEnumerable:
@@ -411,13 +413,20 @@ private void GenerateFastPathFuncForEnumerable(SourceWriter writer, string seria
411413
writer.WriteLine();
412414

413415
string getCurrentElementExpr;
416+
const string elementVarName = "element";
414417
switch (typeGenerationSpec.CollectionType)
415418
{
416419
case CollectionType.Array:
417420
writer.WriteLine($"for (int i = 0; i < {ValueVarName}.Length; i++)");
418421
getCurrentElementExpr = $"{ValueVarName}[i]";
419422
break;
420423

424+
case CollectionType.MemoryOfT:
425+
case CollectionType.ReadOnlyMemoryOfT:
426+
writer.WriteLine($"foreach ({valueTypeGenerationSpec.TypeRef.FullyQualifiedName} {elementVarName} in {ValueVarName}.Span)");
427+
getCurrentElementExpr = elementVarName;
428+
break;
429+
421430
case CollectionType.IListOfT:
422431
case CollectionType.List:
423432
case CollectionType.IList:
@@ -426,7 +435,6 @@ private void GenerateFastPathFuncForEnumerable(SourceWriter writer, string seria
426435
break;
427436

428437
default:
429-
const string elementVarName = "element";
430438
writer.WriteLine($"foreach ({valueTypeGenerationSpec.TypeRef.FullyQualifiedName} {elementVarName} in {ValueVarName})");
431439
getCurrentElementExpr = elementVarName;
432440
break;
@@ -1321,6 +1329,8 @@ private static string GetCollectionInfoMethodName(CollectionType collectionType)
13211329
CollectionType.ConcurrentQueue => "CreateConcurrentQueueInfo",
13221330
CollectionType.ImmutableEnumerable => "CreateImmutableEnumerableInfo",
13231331
CollectionType.IAsyncEnumerableOfT => "CreateIAsyncEnumerableInfo",
1332+
CollectionType.MemoryOfT => "CreateMemoryInfo",
1333+
CollectionType.ReadOnlyMemoryOfT => "CreateReadOnlyMemoryInfo",
13241334
CollectionType.ISet => "CreateISetInfo",
13251335

13261336
CollectionType.Dictionary => "CreateDictionaryInfo",

src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,22 @@ private bool TryResolveCollectionType(
581581
immutableCollectionFactoryTypeFullName = null;
582582
needsRuntimeType = false;
583583

584+
if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.MemoryType))
585+
{
586+
Debug.Assert(!SymbolEqualityComparer.Default.Equals(type, _knownSymbols.MemoryByteType));
587+
valueType = ((INamedTypeSymbol)type).TypeArguments[0];
588+
collectionType = CollectionType.MemoryOfT;
589+
return true;
590+
}
591+
592+
if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.ReadOnlyMemoryType))
593+
{
594+
Debug.Assert(!SymbolEqualityComparer.Default.Equals(type, _knownSymbols.ReadOnlyMemoryByteType));
595+
valueType = ((INamedTypeSymbol)type).TypeArguments[0];
596+
collectionType = CollectionType.ReadOnlyMemoryOfT;
597+
return true;
598+
}
599+
584600
// IAsyncEnumerable<T> takes precedence over IEnumerable.
585601
if (type.GetCompatibleGenericBaseType(_knownSymbols.IAsyncEnumerableOfTType) is INamedTypeSymbol iAsyncEnumerableType)
586602
{
@@ -1449,8 +1465,6 @@ private bool IsUnsupportedType(ITypeSymbol type)
14491465
SymbolEqualityComparer.Default.Equals(_knownSymbols.UIntPtrType, type) ||
14501466
_knownSymbols.MemberInfoType.IsAssignableFrom(type) ||
14511467
_knownSymbols.DelegateType.IsAssignableFrom(type) ||
1452-
SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.MemoryType) ||
1453-
SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.ReadOnlyMemoryType) ||
14541468
type is IArrayTypeSymbol { Rank: > 1 };
14551469
}
14561470

@@ -1474,6 +1488,8 @@ private static HashSet<ITypeSymbol> CreateBuiltInSupportTypeSet(KnownTypeSymbols
14741488
#pragma warning restore
14751489

14761490
AddTypeIfNotNull(knownSymbols.ByteArrayType);
1491+
AddTypeIfNotNull(knownSymbols.MemoryByteType);
1492+
AddTypeIfNotNull(knownSymbols.ReadOnlyMemoryByteType);
14771493
AddTypeIfNotNull(knownSymbols.TimeSpanType);
14781494
AddTypeIfNotNull(knownSymbols.DateTimeOffsetType);
14791495
AddTypeIfNotNull(knownSymbols.DateOnlyType);

src/libraries/System.Text.Json/gen/Model/CollectionType.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public enum CollectionType
2828
IEnumerableOfT,
2929
Stack,
3030
Queue,
31-
ImmutableEnumerable
31+
ImmutableEnumerable,
32+
MemoryOfT,
33+
ReadOnlyMemoryOfT
3234
}
3335
}

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1165,7 +1165,9 @@ public static partial class JsonMetadataServices
11651165
public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonNode?> JsonNodeConverter { get { throw null; } }
11661166
public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonObject?> JsonObjectConverter { get { throw null; } }
11671167
public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonValue?> JsonValueConverter { get { throw null; } }
1168+
public static System.Text.Json.Serialization.JsonConverter<System.Memory<byte>> MemoryByteConverter { get { throw null; } }
11681169
public static System.Text.Json.Serialization.JsonConverter<object?> ObjectConverter { get { throw null; } }
1170+
public static System.Text.Json.Serialization.JsonConverter<System.ReadOnlyMemory<byte>> ReadOnlyMemoryByteConverter { get { throw null; } }
11691171
[System.CLSCompliantAttribute(false)]
11701172
public static System.Text.Json.Serialization.JsonConverter<sbyte> SByteConverter { get { throw null; } }
11711173
public static System.Text.Json.Serialization.JsonConverter<float> SingleConverter { get { throw null; } }
@@ -1196,10 +1198,12 @@ public static partial class JsonMetadataServices
11961198
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateIReadOnlyDictionaryInfo<TCollection, TKey, TValue>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.IReadOnlyDictionary<TKey, TValue> where TKey : notnull { throw null; }
11971199
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.ISet<TElement> { throw null; }
11981200
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateListInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.List<TElement> { throw null; }
1201+
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<Memory<TElement>> CreateMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<Memory<TElement>> collectionInfo) { throw null; }
11991202
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateObjectInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonObjectInfoValues<T> objectInfo) where T : notnull { throw null; }
12001203
public static System.Text.Json.Serialization.Metadata.JsonPropertyInfo CreatePropertyInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonPropertyInfoValues<T> propertyInfo) { throw null; }
12011204
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateQueueInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
12021205
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateQueueInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Queue<TElement> { throw null; }
1206+
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<ReadOnlyMemory<TElement>> CreateReadOnlyMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<ReadOnlyMemory<TElement>> collectionInfo) { throw null; }
12031207
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Action<TCollection, object?> addFunc) where TCollection : System.Collections.IEnumerable { throw null; }
12041208
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateStackInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.Stack<TElement> { throw null; }
12051209
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateValueInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; }

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,14 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
114114
<Compile Include="System\Text\Json\Serialization\Converters\CastingConverter.cs" />
115115
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableDictionaryOfTKeyTValueConverterWithReflection.cs" />
116116
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverterWithReflection.cs" />
117+
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ReadOnlyMemoryConverter.cs" />
118+
<Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverterFactory.cs" />
117119
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOrQueueConverterWithReflection.cs" />
118120
<Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
119121
<Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.Reflection.cs" />
122+
<Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverter.cs" />
123+
<Compile Include="System\Text\Json\Serialization\Converters\Value\ReadOnlyMemoryByteConverter.cs" />
124+
<Compile Include="System\Text\Json\Serialization\Converters\Value\MemoryByteConverter.cs" />
120125
<Compile Include="System\Text\Json\Serialization\Converters\Value\JsonPrimitiveConverter.cs" />
121126
<Compile Include="System\Text\Json\Serialization\IgnoreReferenceResolver.cs" />
122127
<Compile Include="System\Text\Json\Serialization\IJsonOnDeserialized.cs" />
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
namespace System.Text.Json.Serialization.Converters
7+
{
8+
internal sealed class MemoryConverter<T> : JsonCollectionConverter<Memory<T>, T>
9+
{
10+
internal override bool CanHaveMetadata => false;
11+
12+
protected override void Add(in T value, ref ReadStack state)
13+
{
14+
((List<T>)state.Current.ReturnValue!).Add(value);
15+
}
16+
17+
protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options)
18+
{
19+
state.Current.ReturnValue = new List<T>();
20+
}
21+
22+
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
23+
{
24+
Memory<T> memory = ((List<T>)state.Current.ReturnValue!).ToArray().AsMemory();
25+
state.Current.ReturnValue = memory;
26+
}
27+
28+
protected override bool OnWriteResume(Utf8JsonWriter writer, Memory<T> value, JsonSerializerOptions options, ref WriteStack state)
29+
{
30+
int index = state.Current.EnumeratorIndex;
31+
32+
JsonConverter<T> elementConverter = GetElementConverter(ref state);
33+
ReadOnlySpan<T> valueSpan = value.Span;
34+
35+
if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
36+
{
37+
// Fast path that avoids validation and extra indirection.
38+
for (; index < valueSpan.Length; index++)
39+
{
40+
elementConverter.Write(writer, valueSpan[index], options);
41+
}
42+
}
43+
else
44+
{
45+
for (; index < value.Length; index++)
46+
{
47+
T element = valueSpan[index];
48+
if (!elementConverter.TryWrite(writer, element, options, ref state))
49+
{
50+
state.Current.EnumeratorIndex = index;
51+
return false;
52+
}
53+
54+
state.Current.EndCollectionElement();
55+
56+
if (ShouldFlush(writer, ref state))
57+
{
58+
state.Current.EnumeratorIndex = ++index;
59+
return false;
60+
}
61+
}
62+
}
63+
64+
return true;
65+
}
66+
}
67+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics;
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
7+
8+
namespace System.Text.Json.Serialization.Converters
9+
{
10+
[RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
11+
internal sealed class MemoryConverterFactory : JsonConverterFactory
12+
{
13+
public override bool CanConvert(Type typeToConvert)
14+
{
15+
if (!typeToConvert.IsGenericType || !typeToConvert.IsValueType)
16+
{
17+
return false;
18+
}
19+
20+
Type typeDef = typeToConvert.GetGenericTypeDefinition();
21+
return typeDef == typeof(Memory<>) || typeDef == typeof(ReadOnlyMemory<>);
22+
}
23+
24+
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
25+
{
26+
Debug.Assert(CanConvert(typeToConvert));
27+
28+
Type converterType = typeToConvert.GetGenericTypeDefinition() == typeof(Memory<>) ?
29+
typeof(MemoryConverter<>) : typeof(ReadOnlyMemoryConverter<>);
30+
31+
Type elementType = typeToConvert.GetGenericArguments()[0];
32+
33+
return (JsonConverter)Activator.CreateInstance(
34+
converterType.MakeGenericType(elementType))!;
35+
}
36+
}
37+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
namespace System.Text.Json.Serialization.Converters
7+
{
8+
internal sealed class ReadOnlyMemoryConverter<T> : JsonCollectionConverter<ReadOnlyMemory<T>, T>
9+
{
10+
internal override bool CanHaveMetadata => false;
11+
12+
protected override void Add(in T value, ref ReadStack state)
13+
{
14+
((List<T>)state.Current.ReturnValue!).Add(value);
15+
}
16+
17+
protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options)
18+
{
19+
state.Current.ReturnValue = new List<T>();
20+
}
21+
22+
protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
23+
{
24+
ReadOnlyMemory<T> memory = ((List<T>)state.Current.ReturnValue!).ToArray().AsMemory();
25+
state.Current.ReturnValue = memory;
26+
}
27+
28+
protected override bool OnWriteResume(Utf8JsonWriter writer, ReadOnlyMemory<T> value, JsonSerializerOptions options, ref WriteStack state)
29+
{
30+
int index = state.Current.EnumeratorIndex;
31+
32+
JsonConverter<T> elementConverter = GetElementConverter(ref state);
33+
ReadOnlySpan<T> valueSpan = value.Span;
34+
35+
if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
36+
{
37+
// Fast path that avoids validation and extra indirection.
38+
for (; index < valueSpan.Length; index++)
39+
{
40+
elementConverter.Write(writer, valueSpan[index], options);
41+
}
42+
}
43+
else
44+
{
45+
for (; index < value.Length; index++)
46+
{
47+
T element = valueSpan[index];
48+
if (!elementConverter.TryWrite(writer, element, options, ref state))
49+
{
50+
state.Current.EnumeratorIndex = index;
51+
return false;
52+
}
53+
54+
state.Current.EndCollectionElement();
55+
56+
if (ShouldFlush(writer, ref state))
57+
{
58+
state.Current.EnumeratorIndex = ++index;
59+
return false;
60+
}
61+
}
62+
}
63+
64+
return true;
65+
}
66+
}
67+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace System.Text.Json.Serialization.Converters
5+
{
6+
internal sealed class MemoryByteConverter : JsonConverter<Memory<byte>>
7+
{
8+
public override Memory<byte> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9+
{
10+
return reader.GetBytesFromBase64();
11+
}
12+
13+
public override void Write(Utf8JsonWriter writer, Memory<byte> value, JsonSerializerOptions options)
14+
{
15+
writer.WriteBase64StringValue(value.Span);
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)