Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
506bb05
Start adding testcases for IReadOnlySet
sander1095 Oct 1, 2025
80a7bb6
Add .NET Framework exclusions for tests around IReadOnlySet<T>.
sander1095 Oct 1, 2025
419b232
Add another test
sander1095 Oct 1, 2025
ad9f947
Fix typo (Factorty -> Factory)
sander1095 Oct 1, 2025
34ec615
Add another #IF to a test case
sander1095 Oct 1, 2025
4f80f13
Start working on readonlyset converter
sander1095 Oct 1, 2025
5bb7f66
Move IReadOnlySet above ISet so ISet has priority.
sander1095 Oct 1, 2025
195a155
Change #IF pre-processors to PR suggested variant
sander1095 Oct 4, 2025
c5ab159
Move the #IF preprocessor of IReadOnylSetOfTConverter compilation che…
sander1095 Oct 4, 2025
6221944
Start adding support for IReadOnlySetOfT for Source Generators
sander1095 Oct 4, 2025
587f501
Add missing newline
sander1095 Oct 4, 2025
2c17fa1
Remove missing newline as I do not see it on GitHub either, even thou…
sander1095 Oct 4, 2025
bdf9655
Add more code for source generator support
sander1095 Oct 4, 2025
9f0f3fe
Add many more tests based on patterns of existing ISet tests
sander1095 Oct 4, 2025
c3f4662
Fix part of the build issues
sander1095 Oct 4, 2025
91a1ab0
Fix test
sander1095 Oct 4, 2025
c8dc1d3
Add whitespace
sander1095 Oct 9, 2025
5a54b07
Based on the fact that IReadOnlyList does not support population (aft…
sander1095 Oct 9, 2025
fee078f
Delete unit tests that check for CanPopulate support.
Oct 18, 2025
f41d710
Remove more tests that fail due to unsupported casts.
Oct 18, 2025
ee3a3f8
Remove code that should have been removed in previous commits
Oct 18, 2025
6f4714b
Remove another test case because this list isn't filled with read onl…
Oct 18, 2025
deee409
Remove CanPopulate false because this is the default
Oct 18, 2025
9d4b385
Add _isDeserializable to IReadOnlySetOfTConverter for better error me…
Oct 18, 2025
78226c1
Delete more test cases that I believe should fail.
Oct 18, 2025
3052f6b
Add another test case for IReadOnlySet<T>
Oct 18, 2025
361f77e
Add another test case for IReadOnlySet<T>
Oct 18, 2025
ca4534d
Remove unintended formatting changes
Oct 18, 2025
d00b1e9
Switch test class to implementing IReadOnlySet<T> instead of ISet<T>
Oct 18, 2025
309ee17
Add back ISet test case
Oct 18, 2025
3086bfc
Fix .NET 5 reference in comment
Oct 18, 2025
bee1dd5
Remove .NET 5 requirement thanks to PR feedback
sander1095 Nov 3, 2025
ae61d9d
Remove ifdef comment about .NET >= 5 support due to PR feedback
sander1095 Nov 4, 2025
b6e4d51
Remove another comment that I hadnt saved
sander1095 Nov 4, 2025
4fa876b
Remove unnecessary #ifdefs for source generators
sander1095 Nov 4, 2025
4d927bf
Add extra test for IReadOnlySet
sander1095 Nov 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ public KnownTypeSymbols(Compilation compilation)
public INamedTypeSymbol? ISetOfTType => GetOrResolveType(typeof(ISet<>), ref _ISetOfTType);
private Option<INamedTypeSymbol?> _ISetOfTType;

public INamedTypeSymbol? IReadOnlySetOfTType => GetOrResolveType("System.Collections.Generic.IReadOnlySet`1", ref _IReadOnlySetOfTType);
private Option<INamedTypeSymbol?> _IReadOnlySetOfTType;

public INamedTypeSymbol? StackOfTType => GetOrResolveType(typeof(Stack<>), ref _StackOfTType);
private Option<INamedTypeSymbol?> _StackOfTType;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@ private static string GetCollectionInfoMethodName(CollectionType collectionType)
CollectionType.MemoryOfT => "CreateMemoryInfo",
CollectionType.ReadOnlyMemoryOfT => "CreateReadOnlyMemoryInfo",
CollectionType.ISet => "CreateISetInfo",

CollectionType.IReadOnlySetOfT => "CreateIReadOnlySetInfo",
CollectionType.Dictionary => "CreateDictionaryInfo",
CollectionType.IDictionaryOfTKeyTValue or CollectionType.IDictionary => "CreateIDictionaryInfo",
CollectionType.IReadOnlyDictionary => "CreateIReadOnlyDictionaryInfo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,11 @@ private bool TryResolveCollectionType(
collectionType = CollectionType.ISet;
valueType = actualTypeToConvert.TypeArguments[0];
}
else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IReadOnlySetOfTType)) != null)
{
collectionType = CollectionType.IReadOnlySetOfT;
valueType = actualTypeToConvert.TypeArguments[0];
}
else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ICollectionOfTType)) != null)
{
collectionType = CollectionType.ICollectionOfT;
Expand Down
3 changes: 2 additions & 1 deletion src/libraries/System.Text.Json/gen/Model/CollectionType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum CollectionType
Queue,
ImmutableEnumerable,
MemoryOfT,
ReadOnlyMemoryOfT
ReadOnlyMemoryOfT,
IReadOnlySetOfT
}
}
5 changes: 5 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,11 @@ public static partial class JsonMetadataServices
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateImmutableEnumerableInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo, System.Func<System.Collections.Generic.IEnumerable<TElement>, TCollection> createRangeFunc) where TCollection : System.Collections.Generic.IEnumerable<TElement> { throw null; }
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; }

#if NET
public static System.Text.Json.Serialization.Metadata.JsonTypeInfo<TCollection> CreateIReadOnlySetInfo<TCollection, TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<TCollection> collectionInfo) where TCollection : System.Collections.Generic.IReadOnlySet<TElement> { throw null; }
#endif

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<System.Memory<TElement>> CreateMemoryInfo<TElement>(System.Text.Json.JsonSerializerOptions options, System.Text.Json.Serialization.Metadata.JsonCollectionInfoValues<System.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; }
Expand Down
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 @@ -388,6 +388,7 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Serialization\Converters\Value\TimeOnlyConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Value\UInt128Converter.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerOptionsUpdateHandler.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\IReadOnlySetOfTConverter.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
converterType = typeof(ISetOfTConverter<,>);
elementType = actualTypeToConvert.GetGenericArguments()[0];
}
#if NET
// IReadOnlySet<>
else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(IReadOnlySet<>))) != null)
{
converterType = typeof(IReadOnlySetOfTConverter<,>);
elementType = actualTypeToConvert.GetGenericArguments()[0];
}
#endif
// ICollection<>
else if ((actualTypeToConvert = typeToConvert.GetCompatibleGenericInterface(typeof(ICollection<>))) != null)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 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;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class IReadOnlySetOfTConverter<TCollection, TElement>
: IEnumerableDefaultConverter<TCollection, TElement>
where TCollection : IReadOnlySet<TElement>
{
private readonly bool _isDeserializable = typeof(TCollection).IsAssignableFrom(typeof(HashSet<TElement>));

protected override void Add(in TElement value, ref ReadStack state)
{
// Directly convert to HashSet<TElement> since IReadOnlySet<T> does not have an Add method.
HashSet<TElement> collection = (HashSet<TElement>)state.Current.ReturnValue!;
collection.Add(value);
if (IsValueType)
{
state.Current.ReturnValue = collection;
}
}

protected override void CreateCollection(ref Utf8JsonReader reader, scoped ref ReadStack state, JsonSerializerOptions options)
{
if (!_isDeserializable)
{
ThrowHelper.ThrowNotSupportedException_CannotPopulateCollection(Type, ref reader, ref state);
}

state.Current.ReturnValue = new HashSet<TElement>();
}

internal override void ConfigureJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options)
{
// Deserialize as HashSet<TElement> for interface types that support it.
if (jsonTypeInfo.CreateObject is null && Type.IsAssignableFrom(typeof(HashSet<TElement>)))
{
Debug.Assert(Type.IsInterface);
jsonTypeInfo.CreateObject = () => new HashSet<TElement>();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal JsonConverter GetConverterInternal(Type typeToConvert, JsonSerializerOp
ThrowHelper.ThrowInvalidOperationException_SerializerConverterFactoryReturnsNull(GetType());
break;
case JsonConverterFactory:
ThrowHelper.ThrowInvalidOperationException_SerializerConverterFactoryReturnsJsonConverterFactorty(GetType());
ThrowHelper.ThrowInvalidOperationException_SerializerConverterFactoryReturnsJsonConverterFactory(GetType());
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,26 @@ public static JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(
collectionInfo,
new ISetOfTConverter<TCollection, TElement>());

#if NET
/// <summary>
/// Creates serialization metadata for types assignable to <see cref="IReadOnlySet{T}"/>.
/// </summary>
/// <typeparam name="TCollection">The generic definition of the type.</typeparam>
/// <typeparam name="TElement">The generic definition of the element type.</typeparam>
/// <param name="options"></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<TCollection> CreateIReadOnlySetInfo<TCollection, TElement>(
JsonSerializerOptions options,
JsonCollectionInfoValues<TCollection> collectionInfo)
where TCollection : IReadOnlySet<TElement>
=> CreateCore(
options,
collectionInfo,
new IReadOnlySetOfTConverter<TCollection, TElement>());
#endif

/// <summary>
/// Creates serialization metadata for types assignable to <see cref="ICollection{T}"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ public static void ThrowInvalidOperationException_SerializerConverterFactoryRetu
}

[DoesNotReturn]
public static void ThrowInvalidOperationException_SerializerConverterFactoryReturnsJsonConverterFactorty(Type converterType)
public static void ThrowInvalidOperationException_SerializerConverterFactoryReturnsJsonConverterFactory(Type converterType)
{
throw new InvalidOperationException(SR.Format(SR.SerializerConverterFactoryReturnsJsonConverterFactory, converterType));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,86 @@ public async Task ReadSimpleISetT()
Assert.Equal(0, result.Count());
}

#if NET
[Fact]
public async Task ReadNullableGenericStructIReadOnlySetWithNullJson()
{
var wrapper = await Serializer.DeserializeWrapper<GenericStructIReadOnlySetWrapper<int>?>("null");
Assert.False(wrapper.HasValue);
}

[Fact]
public async Task ReadIReadOnlySetTOfHashSetT()
{
IReadOnlySet<HashSet<int>> result = await Serializer.DeserializeWrapper<IReadOnlySet<HashSet<int>>>(@"[[1,2],[3,4]]");

if (result.First().Contains(1))
{
AssertExtensions.Equal(new HashSet<int> { 1, 2 }, result.First());
AssertExtensions.Equal(new HashSet<int> { 3, 4 }, result.Last());
}
else
{
AssertExtensions.Equal(new HashSet<int> { 3, 4 }, result.First());
AssertExtensions.Equal(new HashSet<int> { 1, 2 }, result.Last());
}
}

[Fact]
public async Task ReadHashSetTOfIReadOnlySetT()
{
HashSet<IReadOnlySet<int>> result = await Serializer.DeserializeWrapper<HashSet<IReadOnlySet<int>>>(@"[[1,2],[3,4]]");

if (result.First().Contains(1))
{
Assert.Equal(new HashSet<int> { 1, 2 }, result.First());
Assert.Equal(new HashSet<int> { 3, 4 }, result.Last());
}
else
{
Assert.Equal(new HashSet<int> { 3, 4 }, result.First());
Assert.Equal(new HashSet<int> { 1, 2 }, result.Last());
}
}

[Fact]
public async Task ReadIReadOnlySetTOfArray()
{
IReadOnlySet<int[]> result = await Serializer.DeserializeWrapper<IReadOnlySet<int[]>>(@"[[1,2],[3,4]]");

if (result.First().Contains(1))
{
Assert.Equal(new HashSet<int> { 1, 2 }, result.First());
Assert.Equal(new HashSet<int> { 3, 4 }, result.Last());
}
else
{
Assert.Equal(new HashSet<int> { 3, 4 }, result.First());
Assert.Equal(new HashSet<int> { 1, 2 }, result.Last());
}
}

[Fact]
public async Task ReadArrayOfIReadOnlySetT()
{
IReadOnlySet<int>[] result = await Serializer.DeserializeWrapper<IReadOnlySet<int>[]>(@"[[1,2],[3,4]]");

Assert.Equal(new HashSet<int> { 1, 2 }, result.First());
Assert.Equal(new HashSet<int> { 3, 4 }, result.Last());
}

[Fact]
public async Task ReadSimpleIReadOnlySetT()
{
IReadOnlySet<int> result = await Serializer.DeserializeWrapper<IReadOnlySet<int>>(@"[1,2]");

Assert.Equal(new HashSet<int> { 1, 2 }, result);

result = await Serializer.DeserializeWrapper<IReadOnlySet<int>>(@"[]");
Assert.Equal(0, result.Count());
}
#endif

[Fact]
public async Task StackTOfStackT()
{
Expand Down Expand Up @@ -1136,6 +1216,15 @@ public static IEnumerable<object[]> ReadSimpleTestClass_GenericWrappers_NoAddMet
SimpleTestClassWithStringToStringIReadOnlyDictionaryWrapper.s_json,
typeof(GenericIReadOnlyDictionaryWrapper<string, string>)
};

#if NET
yield return new object[]
{
typeof(SimpleTestClassWithStringIReadOnlySetWrapper),
SimpleTestClassWithStringIReadOnlySetWrapper.s_json,
typeof(WrapperForIReadOnlySetOfT<string>)
};
#endif
}
}

Expand Down Expand Up @@ -1173,6 +1262,12 @@ public async Task Read_HigherOrderCollectionInheritance_Works()
[InlineData(typeof(GenericIListWrapperInternalConstructor<string>), @"[""1""]")]
[InlineData(typeof(GenericISetWrapperPrivateConstructor<string>), @"[""1""]")]
[InlineData(typeof(GenericISetWrapperInternalConstructor<string>), @"[""1""]")]

#if NET
[InlineData(typeof(GenericIReadOnlySetWrapperPrivateConstructor<string>), @"[""1""]")]
[InlineData(typeof(GenericIReadOnlySetWrapperInternalConstructor<string>), @"[""1""]")]
#endif

[InlineData(typeof(GenericIDictionaryWrapperPrivateConstructor<string, string>), @"{""Key"":""Value""}")]
[InlineData(typeof(GenericIDictionaryWrapperInternalConstructor<string, string>), @"{""Key"":""Value""}")]
[InlineData(typeof(StringToStringIReadOnlyDictionaryWrapperPrivateConstructor), @"{""Key"":""Value""}")]
Expand Down Expand Up @@ -1271,6 +1366,11 @@ public static IEnumerable<object[]> CustomInterfaces_Enumerables()
yield return new object[] { typeof(IDerivedICollectionOfT<string>) };
yield return new object[] { typeof(IDerivedIList) };
yield return new object[] { typeof(IDerivedISetOfT<string>) };

#if NET
yield return new object[] { typeof(IDerivedIReadOnlySetOfT<string>) };
#endif

}

public static IEnumerable<object[]> CustomInterfaces_Dictionaries()
Expand Down
Loading
Loading