Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
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,12 @@ public KnownTypeSymbols(Compilation compilation)
public INamedTypeSymbol? ISetOfTType => GetOrResolveType(typeof(ISet<>), ref _ISetOfTType);
private Option<INamedTypeSymbol?> _ISetOfTType;

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#if NET
public INamedTypeSymbol? IReadOnlySetOfTType => GetOrResolveType(typeof(IReadOnlySet<>), ref _IReadOnlySetOfTType);
private Option<INamedTypeSymbol?> _IReadOnlySetOfTType;
#endif

Comment on lines +55 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's incorrect to use #if on target framework in source generator code. The generator itself can run on a different framework (.NET Framework 4.8 when using Visual Studio) than the target framework. See how Memory<T> is handled.

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 @@ -1515,6 +1515,11 @@ private static string GetCollectionInfoMethodName(CollectionType collectionType)
CollectionType.ReadOnlyMemoryOfT => "CreateReadOnlyMemoryInfo",
CollectionType.ISet => "CreateISetInfo",

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#if NET
CollectionType.IReadOnlySetOfT => "CreateIReadOnlySetInfo",
#endif

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,14 @@ private bool TryResolveCollectionType(
collectionType = CollectionType.ISet;
valueType = actualTypeToConvert.TypeArguments[0];
}
// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#if NET
else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.IReadOnlySetOfTType)) != null)
{
collectionType = CollectionType.IReadOnlySetOfT;
valueType = actualTypeToConvert.TypeArguments[0];
}
#endif
else if ((actualTypeToConvert = type.GetCompatibleGenericBaseType(_knownSymbols.ICollectionOfTType)) != null)
{
collectionType = CollectionType.ICollectionOfT;
Expand Down
6 changes: 5 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,10 @@ public enum CollectionType
Queue,
ImmutableEnumerable,
MemoryOfT,
ReadOnlyMemoryOfT
ReadOnlyMemoryOfT,
// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#if NET
IReadOnlySetOfT
#endif
}
}
6 changes: 6 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,12 @@ 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; }

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#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

Comment on lines +1347 to +1352
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public API can't be introduced without going through API review process. We need to go through the review like #86442 and Eirik would be responsible for this.

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
4 changes: 4 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,10 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="$(CoreLibSharedDir)System\Marvin.cs" Link="Common\System\Marvin.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' and $([MSBuild]::VersionGreaterThanOrEquals('$(TargetFrameworkVersion)', '5.0'))">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.NET 5~7 aren't directly targeted by System.Text.Json any more. Just add it into the .NETCoreApp group, together with Int128Converter etc.

Copy link
Author

@sander1095 sander1095 Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would then include .NET Core, which did not have support for IReadOnlySet. The >= net 5.0 check is necessary for successful compilation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.Text.Json doesn't target .NET Core 1.1 ~ 7.0 in current version. No compilation will be affected.

<Compile Include="System\Text\Json\Serialization\Converters\Collection\IReadOnlySetOfTConverter.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(CoreLibSharedDir)System\Collections\Generic\ReferenceEqualityComparer.cs" />
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer
converterType = typeof(ISetOfTConverter<,>);
elementType = actualTypeToConvert.GetGenericArguments()[0];
}
// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#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,27 @@ public static JsonTypeInfo<TCollection> CreateISetInfo<TCollection, TElement>(
collectionInfo,
new ISetOfTConverter<TCollection, TElement>());

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#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,87 @@ public async Task ReadSimpleISetT()
Assert.Equal(0, result.Count());
}

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#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 +1217,16 @@ public static IEnumerable<object[]> ReadSimpleTestClass_GenericWrappers_NoAddMet
SimpleTestClassWithStringToStringIReadOnlyDictionaryWrapper.s_json,
typeof(GenericIReadOnlyDictionaryWrapper<string, string>)
};

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
#if NET
yield return new object[]
{
typeof(SimpleTestClassWithStringIReadOnlySetWrapper),
SimpleTestClassWithStringIReadOnlySetWrapper.s_json,
typeof(WrapperForIReadOnlySetOfT<string>)
};
#endif
}
}

Expand Down Expand Up @@ -1173,6 +1264,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 +1368,12 @@ 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>) };

// Only modern .NET (>= 5.0) supports IReadOnlySet<T>.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: redundant comment; other types that only available on higher frameworks aren't commented like this.

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

}

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