JSON: Add support for {ReadOnly}Memory<T> (#88713)
authorDavid CantĂș <dacantu@microsoft.com>
Thu, 13 Jul 2023 15:01:51 +0000 (10:01 -0500)
committerGitHub <noreply@github.com>
Thu, 13 Jul 2023 15:01:51 +0000 (10:01 -0500)
* Add support for {ReadOnly}Memory

* Address feedback

* Remove unintentional comment

* Register {ReadOnly}MemoryByteType as a known built-in type

23 files changed:
src/libraries/System.Text.Json/gen/Helpers/KnownTypeSymbols.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Emitter.cs
src/libraries/System.Text.Json/gen/JsonSourceGenerator.Parser.cs
src/libraries/System.Text.Json/gen/Model/CollectionType.cs
src/libraries/System.Text.Json/ref/System.Text.Json.cs
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverterFactory.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs [new file with mode: 0644]
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UnsupportedTypeConverterFactory.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/DefaultJsonTypeInfoResolver.Converters.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Collections.cs
src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs
src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Memory.cs [new file with mode: 0644]
src/libraries/System.Text.Json/tests/Common/TestClasses/TestClasses.cs
src/libraries/System.Text.Json/tests/Common/UnsupportedTypesTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/UnsupportedTypesTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets
src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Unit.Tests/JsonSourceGeneratorDiagnosticsTests.cs
src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj

index 137cc5e..99ea95b 100644 (file)
@@ -141,6 +141,18 @@ namespace System.Text.Json.SourceGeneration
 
         private Option<IArrayTypeSymbol?> _ByteArrayType;
 
+        public INamedTypeSymbol? MemoryByteType => _MemoryByteType.HasValue
+            ? _MemoryByteType.Value
+            : (_MemoryByteType = new(MemoryType?.Construct(Compilation.GetSpecialType(SpecialType.System_Byte)))).Value;
+
+        private Option<INamedTypeSymbol?> _MemoryByteType;
+
+        public INamedTypeSymbol? ReadOnlyMemoryByteType => _ReadOnlyMemoryByteType.HasValue
+            ? _ReadOnlyMemoryByteType.Value
+            : (_ReadOnlyMemoryByteType = new(ReadOnlyMemoryType?.Construct(Compilation.GetSpecialType(SpecialType.System_Byte)))).Value;
+
+        private Option<INamedTypeSymbol?> _ReadOnlyMemoryByteType;
+
         public INamedTypeSymbol? GuidType => GetOrResolveType(typeof(Guid), ref _GuidType);
         private Option<INamedTypeSymbol?> _GuidType;
 
index 634d8f6..a6b61db 100644 (file)
@@ -337,6 +337,8 @@ namespace System.Text.Json.SourceGeneration
                 switch (collectionType)
                 {
                     case CollectionType.Array:
+                    case CollectionType.MemoryOfT:
+                    case CollectionType.ReadOnlyMemoryOfT:
                         createCollectionMethodExpr = $"{createCollectionInfoMethodName}<{valueTypeFQN}>({OptionsLocalVariableName}, {InfoVarName})";
                         break;
                     case CollectionType.IEnumerable:
@@ -411,6 +413,7 @@ namespace System.Text.Json.SourceGeneration
                 writer.WriteLine();
 
                 string getCurrentElementExpr;
+                const string elementVarName = "element";
                 switch (typeGenerationSpec.CollectionType)
                 {
                     case CollectionType.Array:
@@ -418,6 +421,12 @@ namespace System.Text.Json.SourceGeneration
                         getCurrentElementExpr = $"{ValueVarName}[i]";
                         break;
 
+                    case CollectionType.MemoryOfT:
+                    case CollectionType.ReadOnlyMemoryOfT:
+                        writer.WriteLine($"foreach ({valueTypeGenerationSpec.TypeRef.FullyQualifiedName} {elementVarName} in {ValueVarName}.Span)");
+                        getCurrentElementExpr = elementVarName;
+                        break;
+
                     case CollectionType.IListOfT:
                     case CollectionType.List:
                     case CollectionType.IList:
@@ -426,7 +435,6 @@ namespace System.Text.Json.SourceGeneration
                         break;
 
                     default:
-                        const string elementVarName = "element";
                         writer.WriteLine($"foreach ({valueTypeGenerationSpec.TypeRef.FullyQualifiedName} {elementVarName} in {ValueVarName})");
                         getCurrentElementExpr = elementVarName;
                         break;
@@ -1321,6 +1329,8 @@ namespace System.Text.Json.SourceGeneration
                     CollectionType.ConcurrentQueue => "CreateConcurrentQueueInfo",
                     CollectionType.ImmutableEnumerable => "CreateImmutableEnumerableInfo",
                     CollectionType.IAsyncEnumerableOfT => "CreateIAsyncEnumerableInfo",
+                    CollectionType.MemoryOfT => "CreateMemoryInfo",
+                    CollectionType.ReadOnlyMemoryOfT => "CreateReadOnlyMemoryInfo",
                     CollectionType.ISet => "CreateISetInfo",
 
                     CollectionType.Dictionary => "CreateDictionaryInfo",
index bc0be7a..423d340 100644 (file)
@@ -581,6 +581,22 @@ namespace System.Text.Json.SourceGeneration
                 immutableCollectionFactoryTypeFullName = null;
                 needsRuntimeType = false;
 
+                if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.MemoryType))
+                {
+                    Debug.Assert(!SymbolEqualityComparer.Default.Equals(type, _knownSymbols.MemoryByteType));
+                    valueType = ((INamedTypeSymbol)type).TypeArguments[0];
+                    collectionType = CollectionType.MemoryOfT;
+                    return true;
+                }
+
+                if (SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.ReadOnlyMemoryType))
+                {
+                    Debug.Assert(!SymbolEqualityComparer.Default.Equals(type, _knownSymbols.ReadOnlyMemoryByteType));
+                    valueType = ((INamedTypeSymbol)type).TypeArguments[0];
+                    collectionType = CollectionType.ReadOnlyMemoryOfT;
+                    return true;
+                }
+
                 // IAsyncEnumerable<T> takes precedence over IEnumerable.
                 if (type.GetCompatibleGenericBaseType(_knownSymbols.IAsyncEnumerableOfTType) is INamedTypeSymbol iAsyncEnumerableType)
                 {
@@ -1449,8 +1465,6 @@ namespace System.Text.Json.SourceGeneration
                     SymbolEqualityComparer.Default.Equals(_knownSymbols.UIntPtrType, type) ||
                     _knownSymbols.MemberInfoType.IsAssignableFrom(type) ||
                     _knownSymbols.DelegateType.IsAssignableFrom(type) ||
-                    SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.MemoryType) ||
-                    SymbolEqualityComparer.Default.Equals(type.OriginalDefinition, _knownSymbols.ReadOnlyMemoryType) ||
                     type is IArrayTypeSymbol { Rank: > 1 };
             }
 
@@ -1474,6 +1488,8 @@ namespace System.Text.Json.SourceGeneration
 #pragma warning restore
 
                 AddTypeIfNotNull(knownSymbols.ByteArrayType);
+                AddTypeIfNotNull(knownSymbols.MemoryByteType);
+                AddTypeIfNotNull(knownSymbols.ReadOnlyMemoryByteType);
                 AddTypeIfNotNull(knownSymbols.TimeSpanType);
                 AddTypeIfNotNull(knownSymbols.DateTimeOffsetType);
                 AddTypeIfNotNull(knownSymbols.DateOnlyType);
index f498cae..7692e09 100644 (file)
@@ -28,6 +28,8 @@ namespace System.Text.Json.SourceGeneration
         IEnumerableOfT,
         Stack,
         Queue,
-        ImmutableEnumerable
+        ImmutableEnumerable,
+        MemoryOfT,
+        ReadOnlyMemoryOfT
     }
 }
index 7a60938..15300ab 100644 (file)
@@ -1165,7 +1165,9 @@ namespace System.Text.Json.Serialization.Metadata
         public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonNode?> JsonNodeConverter { get { throw null; } }
         public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonObject?> JsonObjectConverter { get { throw null; } }
         public static System.Text.Json.Serialization.JsonConverter<System.Text.Json.Nodes.JsonValue?> JsonValueConverter { get { throw null; } }
+        public static System.Text.Json.Serialization.JsonConverter<System.Memory<byte>> MemoryByteConverter { get { throw null; } }
         public static System.Text.Json.Serialization.JsonConverter<object?> ObjectConverter { get { throw null; } }
+        public static System.Text.Json.Serialization.JsonConverter<System.ReadOnlyMemory<byte>> ReadOnlyMemoryByteConverter { get { throw null; } }
         [System.CLSCompliantAttribute(false)]
         public static System.Text.Json.Serialization.JsonConverter<sbyte> SByteConverter { get { throw null; } }
         public static System.Text.Json.Serialization.JsonConverter<float> SingleConverter { get { throw null; } }
@@ -1196,10 +1198,12 @@ namespace System.Text.Json.Serialization.Metadata
         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; }
         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; }
         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; }
+        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; }
         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; }
         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; }
         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; }
         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; }
+        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; }
         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; }
         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; }
         public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<T> CreateValueInfo<T>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.JsonConverter converter) { throw null; }
index 13887ad..49b9be5 100644 (file)
@@ -114,9 +114,14 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
     <Compile Include="System\Text\Json\Serialization\Converters\CastingConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableDictionaryOfTKeyTValueConverterWithReflection.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\ImmutableEnumerableOfTConverterWithReflection.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\ReadOnlyMemoryConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverterFactory.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOrQueueConverterWithReflection.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\JsonMetadataServicesConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Object\ObjectWithParameterizedConstructorConverter.Large.Reflection.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Collection\MemoryConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Value\ReadOnlyMemoryByteConverter.cs" />
+    <Compile Include="System\Text\Json\Serialization\Converters\Value\MemoryByteConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\Converters\Value\JsonPrimitiveConverter.cs" />
     <Compile Include="System\Text\Json\Serialization\IgnoreReferenceResolver.cs" />
     <Compile Include="System\Text\Json\Serialization\IJsonOnDeserialized.cs" />
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverter.cs
new file mode 100644 (file)
index 0000000..853d8a3
--- /dev/null
@@ -0,0 +1,67 @@
+// 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;
+
+namespace System.Text.Json.Serialization.Converters
+{
+    internal sealed class MemoryConverter<T> : JsonCollectionConverter<Memory<T>, T>
+    {
+        internal override bool CanHaveMetadata => false;
+
+        protected override void Add(in T value, ref ReadStack state)
+        {
+            ((List<T>)state.Current.ReturnValue!).Add(value);
+        }
+
+        protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options)
+        {
+            state.Current.ReturnValue = new List<T>();
+        }
+
+        protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
+        {
+            Memory<T> memory = ((List<T>)state.Current.ReturnValue!).ToArray().AsMemory();
+            state.Current.ReturnValue = memory;
+        }
+
+        protected override bool OnWriteResume(Utf8JsonWriter writer, Memory<T> value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            int index = state.Current.EnumeratorIndex;
+
+            JsonConverter<T> elementConverter = GetElementConverter(ref state);
+            ReadOnlySpan<T> valueSpan = value.Span;
+
+            if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
+            {
+                // Fast path that avoids validation and extra indirection.
+                for (; index < valueSpan.Length; index++)
+                {
+                    elementConverter.Write(writer, valueSpan[index], options);
+                }
+            }
+            else
+            {
+                for (; index < value.Length; index++)
+                {
+                    T element = valueSpan[index];
+                    if (!elementConverter.TryWrite(writer, element, options, ref state))
+                    {
+                        state.Current.EnumeratorIndex = index;
+                        return false;
+                    }
+
+                    state.Current.EndCollectionElement();
+
+                    if (ShouldFlush(writer, ref state))
+                    {
+                        state.Current.EnumeratorIndex = ++index;
+                        return false;
+                    }
+                }
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/MemoryConverterFactory.cs
new file mode 100644 (file)
index 0000000..0c2fe8d
--- /dev/null
@@ -0,0 +1,37 @@
+ï»ż// 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.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace System.Text.Json.Serialization.Converters
+{
+    [RequiresDynamicCode(JsonSerializer.SerializationRequiresDynamicCodeMessage)]
+    internal sealed class MemoryConverterFactory : JsonConverterFactory
+    {
+        public override bool CanConvert(Type typeToConvert)
+        {
+            if (!typeToConvert.IsGenericType || !typeToConvert.IsValueType)
+            {
+                return false;
+            }
+
+            Type typeDef = typeToConvert.GetGenericTypeDefinition();
+            return typeDef == typeof(Memory<>) || typeDef == typeof(ReadOnlyMemory<>);
+        }
+
+        public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
+        {
+            Debug.Assert(CanConvert(typeToConvert));
+
+            Type converterType = typeToConvert.GetGenericTypeDefinition() == typeof(Memory<>) ?
+                typeof(MemoryConverter<>) : typeof(ReadOnlyMemoryConverter<>);
+
+            Type elementType = typeToConvert.GetGenericArguments()[0];
+
+            return (JsonConverter)Activator.CreateInstance(
+                converterType.MakeGenericType(elementType))!;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/ReadOnlyMemoryConverter.cs
new file mode 100644 (file)
index 0000000..6d5a39e
--- /dev/null
@@ -0,0 +1,67 @@
+// 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;
+
+namespace System.Text.Json.Serialization.Converters
+{
+    internal sealed class ReadOnlyMemoryConverter<T> : JsonCollectionConverter<ReadOnlyMemory<T>, T>
+    {
+        internal override bool CanHaveMetadata => false;
+
+        protected override void Add(in T value, ref ReadStack state)
+        {
+            ((List<T>)state.Current.ReturnValue!).Add(value);
+        }
+
+        protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options)
+        {
+            state.Current.ReturnValue = new List<T>();
+        }
+
+        protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
+        {
+            ReadOnlyMemory<T> memory = ((List<T>)state.Current.ReturnValue!).ToArray().AsMemory();
+            state.Current.ReturnValue = memory;
+        }
+
+        protected override bool OnWriteResume(Utf8JsonWriter writer, ReadOnlyMemory<T> value, JsonSerializerOptions options, ref WriteStack state)
+        {
+            int index = state.Current.EnumeratorIndex;
+
+            JsonConverter<T> elementConverter = GetElementConverter(ref state);
+            ReadOnlySpan<T> valueSpan = value.Span;
+
+            if (elementConverter.CanUseDirectReadOrWrite && state.Current.NumberHandling == null)
+            {
+                // Fast path that avoids validation and extra indirection.
+                for (; index < valueSpan.Length; index++)
+                {
+                    elementConverter.Write(writer, valueSpan[index], options);
+                }
+            }
+            else
+            {
+                for (; index < value.Length; index++)
+                {
+                    T element = valueSpan[index];
+                    if (!elementConverter.TryWrite(writer, element, options, ref state))
+                    {
+                        state.Current.EnumeratorIndex = index;
+                        return false;
+                    }
+
+                    state.Current.EndCollectionElement();
+
+                    if (ShouldFlush(writer, ref state))
+                    {
+                        state.Current.EnumeratorIndex = ++index;
+                        return false;
+                    }
+                }
+            }
+
+            return true;
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs
new file mode 100644 (file)
index 0000000..20536c8
--- /dev/null
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.Serialization.Converters
+{
+    internal sealed class MemoryByteConverter : JsonConverter<Memory<byte>>
+    {
+        public override Memory<byte> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            return reader.GetBytesFromBase64();
+        }
+
+        public override void Write(Utf8JsonWriter writer, Memory<byte> value, JsonSerializerOptions options)
+        {
+            writer.WriteBase64StringValue(value.Span);
+        }
+    }
+}
diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs
new file mode 100644 (file)
index 0000000..68b7c1f
--- /dev/null
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Text.Json.Serialization.Converters
+{
+    internal sealed class ReadOnlyMemoryByteConverter : JsonConverter<ReadOnlyMemory<byte>>
+    {
+        public override ReadOnlyMemory<byte> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            return reader.GetBytesFromBase64();
+        }
+
+        public override void Write(Utf8JsonWriter writer, ReadOnlyMemory<byte> value, JsonSerializerOptions options)
+        {
+            writer.WriteBase64StringValue(value.Span);
+        }
+    }
+}
index 698e2c3..82c3216 100644 (file)
@@ -26,21 +26,8 @@ namespace System.Text.Json.Serialization.Converters
                 type == typeof(SerializationInfo) ||
                 type == typeof(IntPtr) ||
                 type == typeof(UIntPtr) ||
-                // Exclude Memory<T> and ReadOnlyMemory<T> types.
-                IsMemoryType(type) ||
                 // Exclude delegates.
                 typeof(Delegate).IsAssignableFrom(type);
-
-            static bool IsMemoryType(Type type)
-            {
-                if (!type.IsGenericType || !type.IsValueType)
-                {
-                    return false;
-                }
-
-                Type typeDef = type.GetGenericTypeDefinition();
-                return typeDef == typeof(Memory<>) || typeDef == typeof(ReadOnlyMemory<>);
-            }
         }
 
         public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
index 0b25768..7516c3f 100644 (file)
@@ -28,6 +28,7 @@ namespace System.Text.Json.Serialization.Metadata
                 new EnumConverterFactory(),
                 new JsonNodeConverterFactory(),
                 new FSharpTypeConverterFactory(),
+                new MemoryConverterFactory(),
                 // IAsyncEnumerable takes precedence over IEnumerable.
                 new IAsyncEnumerableConverterFactory(),
                 // IEnumerable should always be second to last since they can convert any IEnumerable.
@@ -39,7 +40,7 @@ namespace System.Text.Json.Serialization.Metadata
 
         private static Dictionary<Type, JsonConverter> GetDefaultSimpleConverters()
         {
-            const int NumberOfSimpleConverters = 26;
+            const int NumberOfSimpleConverters = 28;
             var converters = new Dictionary<Type, JsonConverter>(NumberOfSimpleConverters);
 
             // Use a dictionary for simple converters.
@@ -62,6 +63,8 @@ namespace System.Text.Json.Serialization.Metadata
             Add(JsonMetadataServices.Int64Converter);
             Add(JsonMetadataServices.JsonElementConverter);
             Add(JsonMetadataServices.JsonDocumentConverter);
+            Add(JsonMetadataServices.MemoryByteConverter);
+            Add(JsonMetadataServices.ReadOnlyMemoryByteConverter);
             Add(JsonMetadataServices.ObjectConverter);
             Add(JsonMetadataServices.SByteConverter);
             Add(JsonMetadataServices.SingleConverter);
index 47d7163..36faca3 100644 (file)
@@ -431,5 +431,33 @@ namespace System.Text.Json.Serialization.Metadata
                 options,
                 collectionInfo,
                 new IEnumerableConverter<TCollection>());
+
+        /// <summary>
+        /// Creates serialization metadata for <see cref="Memory{T}"/>.
+        /// </summary>
+        /// <typeparam name="TElement">The generic definition of the element type.</typeparam>
+        /// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
+        /// <param name="collectionInfo">Provides serialization metadata about the collection type.</param>
+        /// <returns>Serialization metadata for the given type.</returns>
+        /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
+        public static JsonTypeInfo<Memory<TElement>> CreateMemoryInfo<TElement>(JsonSerializerOptions options, JsonCollectionInfoValues<Memory<TElement>> collectionInfo)
+            => CreateCore(
+                options,
+                collectionInfo,
+                new MemoryConverter<TElement>());
+
+        /// <summary>
+        /// Creates serialization metadata for <see cref="ReadOnlyMemory{T}"/>.
+        /// </summary>
+        /// <typeparam name="TElement">The generic definition of the element type.</typeparam>
+        /// <param name="options">The <see cref="JsonSerializerOptions"/> to use.</param>
+        /// <param name="collectionInfo">Provides serialization metadata about the collection type.</param>
+        /// <returns>Serialization metadata for the given type.</returns>
+        /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
+        public static JsonTypeInfo<ReadOnlyMemory<TElement>> CreateReadOnlyMemoryInfo<TElement>(JsonSerializerOptions options, JsonCollectionInfoValues<ReadOnlyMemory<TElement>> collectionInfo)
+            => CreateCore(
+                options,
+                collectionInfo,
+                new ReadOnlyMemoryConverter<TElement>());
     }
 }
index 6377bab..58c2f2a 100644 (file)
@@ -151,6 +151,20 @@ namespace System.Text.Json.Serialization.Metadata
         private static JsonConverter<JsonDocument?>? s_jsonDocumentConverter;
 
         /// <summary>
+        /// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="Memory{Byte}"/> values.
+        /// </summary>
+        /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
+        public static JsonConverter<Memory<byte>> MemoryByteConverter => s_memoryByteConverter ??= new MemoryByteConverter();
+        private static JsonConverter<Memory<byte>>? s_memoryByteConverter;
+
+        /// <summary>
+        /// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="ReadOnlyMemory{Byte}"/> values.
+        /// </summary>
+        /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
+        public static JsonConverter<ReadOnlyMemory<byte>> ReadOnlyMemoryByteConverter => s_readOnlyMemoryByteConverter ??= new ReadOnlyMemoryByteConverter();
+        private static JsonConverter<ReadOnlyMemory<byte>>? s_readOnlyMemoryByteConverter;
+
+        /// <summary>
         /// Returns a <see cref="JsonConverter{T}"/> instance that converts <see cref="object"/> values.
         /// </summary>
         /// <remarks>This API is for use by the output of the System.Text.Json source generator and should not be called directly.</remarks>
diff --git a/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Memory.cs b/src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Memory.cs
new file mode 100644 (file)
index 0000000..bf6e148
--- /dev/null
@@ -0,0 +1,126 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json.Tests;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Text.Json.Serialization.Tests
+{
+    public abstract partial class CollectionTests
+    {
+        private static readonly byte[] s_testData = "This is some test data!!!"u8.ToArray();
+
+        [Fact]
+        public async Task SerializeMemoryOfTAsync()
+        {
+            Memory<int> memoryInt = new int[] { 1, 2, 3 }.AsMemory();
+            Assert.Equal("[1,2,3]", await Serializer.SerializeWrapper(memoryInt));
+
+            Memory<EmptyClass> memoryPoco = new EmptyClass[] { new(), new(), new() }.AsMemory();
+            Assert.Equal("[{},{},{}]", await Serializer.SerializeWrapper(memoryPoco));
+        }
+
+        [Fact]
+        public async Task SerializeReadOnlyMemoryOfTAsync()
+        {
+            ReadOnlyMemory<int> memoryInt = new int[] { 1, 2, 3 }.AsMemory();
+            Assert.Equal("[1,2,3]", await Serializer.SerializeWrapper(memoryInt));
+
+            ReadOnlyMemory<EmptyClass> memoryPoco = new EmptyClass[] { new(), new(), new() }.AsMemory();
+            Assert.Equal("[{},{},{}]", await Serializer.SerializeWrapper(memoryPoco));
+        }
+
+        [Fact]
+        public async Task DeserializeMemoryOfTAsync()
+        {
+            Memory<int> memoryInt = await Serializer.DeserializeWrapper<Memory<int>>("[1,2,3]");
+            AssertExtensions.SequenceEqual(new int[] { 1, 2, 3 }, memoryInt.Span);
+
+            Memory<EmptyClass> memoryPoco = new EmptyClass[] { new(), new(), new() }.AsMemory();
+            Assert.Equal(3, memoryPoco.Length);
+        }
+
+        [Fact]
+        public async Task DeserializeReadOnlyMemoryOfTAsync()
+        {
+            ReadOnlyMemory<int> memoryInt = await Serializer.DeserializeWrapper<ReadOnlyMemory<int>>("[1,2,3]");
+            AssertExtensions.SequenceEqual(new int[] { 1, 2, 3 }, memoryInt.Span);
+
+            ReadOnlyMemory<EmptyClass> memoryPoco = await Serializer.DeserializeWrapper<ReadOnlyMemory<EmptyClass>>("[{},{},{}]");
+            Assert.Equal(3, memoryPoco.Length);
+        }
+
+        [Fact]
+        public async Task SerializeMemoryOfTClassAsync()
+        {
+            MemoryOfTClass<int> memoryOfIntClass = new MemoryOfTClass<int>();
+            memoryOfIntClass.Memory = new int[] { 1, 2, 3 };
+
+            string json = await Serializer.SerializeWrapper(memoryOfIntClass);
+            Assert.Equal(@"{""Memory"":[1,2,3]}", json);
+        }
+
+        [Fact]
+        public async Task SerializeReadOnlyMemoryOfTClassAsync()
+        {
+            ReadOnlyMemoryOfTClass<int> memoryOfIntClass = new ReadOnlyMemoryOfTClass<int>();
+            memoryOfIntClass.ReadOnlyMemory = new int[] { 1, 2, 3 };
+
+            string json = await Serializer.SerializeWrapper(memoryOfIntClass);
+            Assert.Equal(@"{""ReadOnlyMemory"":[1,2,3]}", json);
+        }
+
+        [Fact]
+        public async Task DeserializeMemoryOfTClassAsync()
+        {
+            string json = @"{""Memory"":[1,2,3]}";
+            MemoryOfTClass<int> memoryOfIntClass = await Serializer.DeserializeWrapper<MemoryOfTClass<int>>(json);
+            AssertExtensions.SequenceEqual(new int[] { 1, 2, 3 }, memoryOfIntClass.Memory.Span);
+        }
+
+        [Fact]
+        public async Task DeserializeReadOnlyMemoryOfTClassAsync()
+        {
+            string json = @"{""ReadOnlyMemory"":[1,2,3]}";
+            ReadOnlyMemoryOfTClass<int> memoryOfIntClass = await Serializer.DeserializeWrapper<ReadOnlyMemoryOfTClass<int>>(json);
+            AssertExtensions.SequenceEqual(new int[] { 1, 2, 3 }, memoryOfIntClass.ReadOnlyMemory.Span);
+        }
+
+        [Fact]
+        public async Task SerializeMemoryByteAsync()
+        {
+            Assert.Equal("\"VGhpcyBpcyBzb21lIHRlc3QgZGF0YSEhIQ==\"", await Serializer.SerializeWrapper<Memory<byte>>(s_testData.AsMemory()));
+            Assert.Equal("\"VGhpcyBpcyBzb21lIHRlc3QgZGF0YSEhIQ==\"", await Serializer.SerializeWrapper<ReadOnlyMemory<byte>>(s_testData.AsMemory()));
+        }
+
+        [Fact]
+        public async Task DeserializeMemoryByteAsync()
+        {
+            Memory<byte> memory = await Serializer.DeserializeWrapper<Memory<byte>>("\"VGhpcyBpcyBzb21lIHRlc3QgZGF0YSEhIQ==\"");
+            AssertExtensions.SequenceEqual(s_testData, memory.Span);
+
+            ReadOnlyMemory<byte> readOnlyMemory = await Serializer.DeserializeWrapper<ReadOnlyMemory<byte>>("\"VGhpcyBpcyBzb21lIHRlc3QgZGF0YSEhIQ==\"");
+            AssertExtensions.SequenceEqual(s_testData, readOnlyMemory.Span);
+        }
+
+        [Fact]
+        public async Task SerializeMemoryByteClassAsync()
+        {
+            MemoryOfTClass<byte> memoryOfByteClass = new MemoryOfTClass<byte>();
+            memoryOfByteClass.Memory = s_testData;
+
+            string json = await Serializer.SerializeWrapper(memoryOfByteClass);
+            Assert.Equal(@"{""Memory"":""VGhpcyBpcyBzb21lIHRlc3QgZGF0YSEhIQ==""}", json);
+        }
+
+        [Fact]
+        public async Task DeserializeMemoryByteClassAsync()
+        {
+            string json = @"{""Memory"":""VGhpcyBpcyBzb21lIHRlc3QgZGF0YSEhIQ==""}";
+
+            MemoryOfTClass<byte> memoryOfByteClass = await Serializer.DeserializeWrapper<MemoryOfTClass<byte>>(json);
+            AssertExtensions.SequenceEqual<byte>(s_testData, memoryOfByteClass.Memory.Span);
+        }
+    }
+}
index dfe51ae..79859c1 100644 (file)
@@ -2293,4 +2293,14 @@ namespace System.Text.Json.Serialization.Tests
         public List<ClassWithRecursiveCollectionTypes> List { get; set; }
         public IReadOnlyDictionary<string, ClassWithRecursiveCollectionTypes>? Dictionary { get; set; }
     }
+
+    internal class MemoryOfTClass<T>
+    {
+        public Memory<T> Memory { get; set; }
+    }
+
+    internal class ReadOnlyMemoryOfTClass<T>
+    {
+        public ReadOnlyMemory<T> ReadOnlyMemory { get; set; }
+    }
 }
index 7f47afa..79ee255 100644 (file)
@@ -129,8 +129,6 @@ namespace System.Text.Json.Serialization.Tests
             yield return WrapArgs((IntPtr)123);
             yield return WrapArgs<IntPtr?>(new IntPtr(123)); // One nullable variation.
             yield return WrapArgs((UIntPtr)123);
-            yield return WrapArgs((Memory<byte>)new byte[] { 1, 2, 3 });
-            yield return WrapArgs((ReadOnlyMemory<byte>)new byte[] { 1, 2, 3 });
 
             static object[] WrapArgs<T>(T value) => new object[] { new ValueWrapper<T>(value) };
         }
index 43d8bf9..0d0c69d 100644 (file)
@@ -454,6 +454,12 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(KeyValuePair<string, KeyValuePair<string, int>>))]
         [JsonSerializable(typeof(StackWrapper))]
         [JsonSerializable(typeof(ClassWithRecursiveCollectionTypes))]
+        [JsonSerializable(typeof(MemoryOfTClass<byte>))]
+        [JsonSerializable(typeof(ReadOnlyMemoryOfTClass<byte>))]
+        [JsonSerializable(typeof(MemoryOfTClass<int>))]
+        [JsonSerializable(typeof(ReadOnlyMemoryOfTClass<int>))]
+        [JsonSerializable(typeof(MemoryOfTClass<EmptyClass>))]
+        [JsonSerializable(typeof(ReadOnlyMemoryOfTClass<EmptyClass>))]
         internal sealed partial class CollectionTestsContext_Metadata : JsonSerializerContext
         {
         }
@@ -857,6 +863,12 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(KeyValuePair<string, KeyValuePair<string, int>>))]
         [JsonSerializable(typeof(StackWrapper))]
         [JsonSerializable(typeof(ClassWithRecursiveCollectionTypes))]
+        [JsonSerializable(typeof(MemoryOfTClass<byte>))]
+        [JsonSerializable(typeof(ReadOnlyMemoryOfTClass<byte>))]
+        [JsonSerializable(typeof(MemoryOfTClass<int>))]
+        [JsonSerializable(typeof(ReadOnlyMemoryOfTClass<int>))]
+        [JsonSerializable(typeof(MemoryOfTClass<EmptyClass>))]
+        [JsonSerializable(typeof(ReadOnlyMemoryOfTClass<EmptyClass>))]
         internal sealed partial class CollectionTestsContext_Default : JsonSerializerContext
         {
         }
index 6220538..dece192 100644 (file)
@@ -42,10 +42,6 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(ClassWithType<IntPtr?>))]
         [JsonSerializable(typeof(UIntPtr))]
         [JsonSerializable(typeof(ClassWithType<UIntPtr>))]
-        [JsonSerializable(typeof(Memory<byte>))]
-        [JsonSerializable(typeof(ClassWithType<Memory<byte>>))]
-        [JsonSerializable(typeof(ReadOnlyMemory<byte>))]
-        [JsonSerializable(typeof(ClassWithType<ReadOnlyMemory<byte>>))]
         [JsonSerializable(typeof(IAsyncEnumerable<int>))]
         [JsonSerializable(typeof(ClassWithType<IAsyncEnumerable<int>>))]
         [JsonSerializable(typeof(ClassThatImplementsIAsyncEnumerable))]
@@ -88,10 +84,6 @@ namespace System.Text.Json.SourceGeneration.Tests
         [JsonSerializable(typeof(ClassWithType<IntPtr?>))]
         [JsonSerializable(typeof(UIntPtr))]
         [JsonSerializable(typeof(ClassWithType<UIntPtr>))]
-        [JsonSerializable(typeof(Memory<byte>))]
-        [JsonSerializable(typeof(ClassWithType<Memory<byte>>))]
-        [JsonSerializable(typeof(ReadOnlyMemory<byte>))]
-        [JsonSerializable(typeof(ClassWithType<ReadOnlyMemory<byte>>))]
         [JsonSerializable(typeof(IAsyncEnumerable<int>))]
         [JsonSerializable(typeof(ClassWithType<IAsyncEnumerable<int>>))]
         [JsonSerializable(typeof(ClassThatImplementsIAsyncEnumerable))]
index 8f891d3..c401cbe 100644 (file)
@@ -61,6 +61,7 @@
     <Compile Include="..\Common\CollectionTests\CollectionTests.Generic.Write.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.Generic.Write.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.Immutable.Read.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.Immutable.Read.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.KeyValuePair.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.KeyValuePair.cs" />
+    <Compile Include="..\Common\CollectionTests\CollectionTests.Memory.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.Memory.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.NonGeneric.Read.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.NonGeneric.Read.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.NonGeneric.Write.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.NonGeneric.Write.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.ObjectModel.Read.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.ObjectModel.Read.cs" />
index debc403..92b7c03 100644 (file)
@@ -422,6 +422,57 @@ namespace System.Text.Json.SourceGeneration.UnitTests
         }
 
         [Theory]
+        [InlineData(LanguageVersion.Default)]
+        [InlineData(LanguageVersion.Preview)]
+        [InlineData(LanguageVersion.Latest)]
+        [InlineData(LanguageVersion.LatestMajor)]
+        [InlineData(LanguageVersion.CSharp9)]
+#if ROSLYN4_4_OR_GREATER
+        [InlineData(LanguageVersion.CSharp10)]
+        [InlineData(LanguageVersion.CSharp11)]
+#endif
+        public void SupportedLanguageVersions_Memory_SucceedCompilation(LanguageVersion langVersion)
+        {
+            string source = """
+                using System;
+                using System.Text.Json.Serialization;
+
+                namespace HelloWorld
+                { 
+                    public class MyClass<T>
+                    {
+                        public MyClass(
+                            Memory<T> memoryOfT,
+                            Memory<byte> memoryByte,
+                            ReadOnlyMemory<T> readOnlyMemoryOfT,
+                            ReadOnlyMemory<byte> readOnlyMemoryByte)
+                        {
+                            MemoryOfT = memoryOfT;
+                            MemoryByte = memoryByte;
+                            ReadOnlyMemoryOfT = readOnlyMemoryOfT;
+                            ReadOnlyMemoryByte = readOnlyMemoryByte;
+                        }
+
+                        public Memory<T> MemoryOfT { get; set; }
+                        public Memory<byte> MemoryByte { get; set; }
+                        public ReadOnlyMemory<T> ReadOnlyMemoryOfT { get; set; }
+                        public ReadOnlyMemory<byte> ReadOnlyMemoryByte { get; set; }
+                    }
+
+                    [JsonSerializable(typeof(MyClass<int>))]
+                    public partial class MyJsonContext : JsonSerializerContext
+                    {
+                    }
+                }
+                """;
+
+            CSharpParseOptions parseOptions = CompilationHelper.CreateParseOptions(langVersion);
+            Compilation compilation = CompilationHelper.CreateCompilation(source, parseOptions: parseOptions);
+
+            CompilationHelper.RunJsonSourceGenerator(compilation);
+        }
+
+        [Theory]
         [InlineData(LanguageVersion.CSharp1)]
         [InlineData(LanguageVersion.CSharp2)]
         [InlineData(LanguageVersion.CSharp3)]
index 5e3cf48..2b79fd9 100644 (file)
@@ -39,6 +39,7 @@
     <Compile Include="..\Common\CollectionTests\CollectionTests.Generic.Write.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.Generic.Write.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.Immutable.Read.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.Immutable.Read.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.KeyValuePair.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.KeyValuePair.cs" />
+    <Compile Include="..\Common\CollectionTests\CollectionTests.Memory.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.Memory.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.NonGeneric.Read.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.NonGeneric.Read.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.NonGeneric.Write.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.NonGeneric.Write.cs" />
     <Compile Include="..\Common\CollectionTests\CollectionTests.ObjectModel.Read.cs" Link="CommonTest\System\Text\Json\Tests\Serialization\CollectionTests\CollectionTests.ObjectModel.Read.cs" />