Skip to content

Commit a54d9e9

Browse files
STJ: Dispose enumerator on exception (#100194)
* STJ: Dispose enumerator on exception * Avoid code duplication * Rework fix * Remove useless Disposable field * Apply fix on all collection converters * Remove duplicate assignments * Skip fix for no-op Dispose implementation * Move IEnumerator disposal to WriteCore method. --------- Co-authored-by: Eirik Tsarpalis <eirik.tsarpalis@gmail.com>
1 parent ddcbc8b commit a54d9e9

File tree

11 files changed

+68
-37
lines changed

11 files changed

+68
-37
lines changed

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/DictionaryDefaultConverter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ protected internal override bool OnWriteResume(
2828
if (state.Current.CollectionEnumerator == null)
2929
{
3030
enumerator = value.GetEnumerator();
31+
state.Current.CollectionEnumerator = enumerator;
3132
if (!enumerator.MoveNext())
3233
{
3334
enumerator.Dispose();
@@ -47,7 +48,6 @@ protected internal override bool OnWriteResume(
4748
{
4849
if (ShouldFlush(ref state, writer))
4950
{
50-
state.Current.CollectionEnumerator = enumerator;
5151
return false;
5252
}
5353

@@ -61,7 +61,6 @@ protected internal override bool OnWriteResume(
6161
TValue element = enumerator.Current.Value;
6262
if (!_valueConverter.TryWrite(writer, element, options, ref state))
6363
{
64-
state.Current.CollectionEnumerator = enumerator;
6564
return false;
6665
}
6766

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IDictionaryConverter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TDictionar
4545
if (state.Current.CollectionEnumerator == null)
4646
{
4747
enumerator = value.GetEnumerator();
48+
state.Current.CollectionEnumerator = enumerator;
4849
if (!enumerator.MoveNext())
4950
{
5051
return true;
@@ -62,7 +63,6 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TDictionar
6263
{
6364
if (ShouldFlush(ref state, writer))
6465
{
65-
state.Current.CollectionEnumerator = enumerator;
6666
return false;
6767
}
6868

@@ -87,7 +87,6 @@ protected internal override bool OnWriteResume(Utf8JsonWriter writer, TDictionar
8787
object? element = enumerator.Value;
8888
if (!_valueConverter.TryWrite(writer, element, options, ref state))
8989
{
90-
state.Current.CollectionEnumerator = enumerator;
9190
return false;
9291
}
9392

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableConverter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ protected override bool OnWriteResume(
4646
if (state.Current.CollectionEnumerator == null)
4747
{
4848
enumerator = value.GetEnumerator();
49+
state.Current.CollectionEnumerator = enumerator;
4950
if (!enumerator.MoveNext())
5051
{
5152
return true;
@@ -61,14 +62,12 @@ protected override bool OnWriteResume(
6162
{
6263
if (ShouldFlush(ref state, writer))
6364
{
64-
state.Current.CollectionEnumerator = enumerator;
6565
return false;
6666
}
6767

6868
object? element = enumerator.Current;
6969
if (!converter.TryWrite(writer, element, options, ref state))
7070
{
71-
state.Current.CollectionEnumerator = enumerator;
7271
return false;
7372
}
7473

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/IEnumerableDefaultConverter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value,
2222
if (state.Current.CollectionEnumerator == null)
2323
{
2424
enumerator = value.GetEnumerator();
25+
state.Current.CollectionEnumerator = enumerator;
2526
if (!enumerator.MoveNext())
2627
{
2728
enumerator.Dispose();
@@ -39,14 +40,12 @@ protected override bool OnWriteResume(Utf8JsonWriter writer, TCollection value,
3940
{
4041
if (ShouldFlush(ref state, writer))
4142
{
42-
state.Current.CollectionEnumerator = enumerator;
4343
return false;
4444
}
4545

4646
TElement element = enumerator.Current;
4747
if (!converter.TryWrite(writer, element, options, ref state))
4848
{
49-
state.Current.CollectionEnumerator = enumerator;
5049
return false;
5150
}
5251

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Collection/StackOrQueueConverter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ protected sealed override bool OnWriteResume(Utf8JsonWriter writer, TCollection
4646
if (state.Current.CollectionEnumerator == null)
4747
{
4848
enumerator = value.GetEnumerator();
49+
state.Current.CollectionEnumerator = enumerator;
4950
if (!enumerator.MoveNext())
5051
{
5152
return true;
@@ -61,14 +62,12 @@ protected sealed override bool OnWriteResume(Utf8JsonWriter writer, TCollection
6162
{
6263
if (ShouldFlush(ref state, writer))
6364
{
64-
state.Current.CollectionEnumerator = enumerator;
6565
return false;
6666
}
6767

6868
object? element = enumerator.Current;
6969
if (!converter.TryWrite(writer, element, options, ref state))
7070
{
71-
state.Current.CollectionEnumerator = enumerator;
7271
return false;
7372
}
7473

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.WriteCore.cs

+26-21
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,37 @@ internal bool WriteCore(
1515
{
1616
return TryWrite(writer, value, options, ref state);
1717
}
18-
catch (InvalidOperationException ex) when (ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException)
18+
catch (Exception ex)
1919
{
20-
ThrowHelper.ReThrowWithPath(ref state, ex);
21-
throw;
22-
}
23-
catch (JsonException ex) when (ex.Path == null)
24-
{
25-
// JsonExceptions where the Path property is already set
26-
// typically originate from nested calls to JsonSerializer;
27-
// treat these cases as any other exception type and do not
28-
// overwrite any exception information.
20+
if (!state.SupportAsync)
21+
{
22+
// Async serializers should dispose sync and
23+
// async disposables from the async root method.
24+
state.DisposePendingDisposablesOnException();
25+
}
2926

30-
ThrowHelper.AddJsonExceptionInformation(ref state, ex);
31-
throw;
32-
}
33-
catch (NotSupportedException ex)
34-
{
35-
// If the message already contains Path, just re-throw. This could occur in serializer re-entry cases.
36-
// To get proper Path semantics in re-entry cases, APIs that take 'state' need to be used.
37-
if (ex.Message.Contains(" Path: "))
27+
switch (ex)
3828
{
39-
throw;
29+
case InvalidOperationException when ex.Source == ThrowHelper.ExceptionSourceValueToRethrowAsJsonException:
30+
ThrowHelper.ReThrowWithPath(ref state, ex);
31+
break;
32+
33+
case JsonException { Path: null } jsonException:
34+
// JsonExceptions where the Path property is already set
35+
// typically originate from nested calls to JsonSerializer;
36+
// treat these cases as any other exception type and do not
37+
// overwrite any exception information.
38+
ThrowHelper.AddJsonExceptionInformation(ref state, jsonException);
39+
break;
40+
41+
case NotSupportedException when !ex.Message.Contains(" Path: "):
42+
// If the message already contains Path, just re-throw. This could occur in serializer re-entry cases.
43+
// To get proper Path semantics in re-entry cases, APIs that take 'state' need to be used.
44+
ThrowHelper.ThrowNotSupportedException(ref state, ex);
45+
break;
4046
}
4147

42-
ThrowHelper.ThrowNotSupportedException(ref state, ex);
43-
return default;
48+
throw;
4449
}
4550
}
4651
}

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfoOfT.WriteHelpers.cs

+1
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ rootValue is not null &&
305305
{
306306
ThrowHelper.ThrowInvalidOperationException_PipeWriterDoesNotImplementUnflushedBytes(bufferWriter);
307307
}
308+
308309
state.PipeWriter = bufferWriter;
309310
state.FlushThreshold = (int)(bufferWriter.Capacity * JsonSerializer.FlushThreshold);
310311

src/libraries/System.Text.Json/src/System/Text/Json/Serialization/WriteStack.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ public void AddCompletedAsyncDisposable(IAsyncDisposable asyncDisposable)
278278
=> (CompletedAsyncDisposables ??= new List<IAsyncDisposable>()).Add(asyncDisposable);
279279

280280
// Asynchronously dispose of any AsyncDisposables that have been scheduled for disposal
281-
public async ValueTask DisposeCompletedAsyncDisposables()
281+
public readonly async ValueTask DisposeCompletedAsyncDisposables()
282282
{
283283
Debug.Assert(CompletedAsyncDisposables?.Count > 0);
284284
Exception? exception = null;
@@ -307,7 +307,7 @@ public async ValueTask DisposeCompletedAsyncDisposables()
307307
/// Walks the stack cleaning up any leftover IDisposables
308308
/// in the event of an exception on serialization
309309
/// </summary>
310-
public void DisposePendingDisposablesOnException()
310+
public readonly void DisposePendingDisposablesOnException()
311311
{
312312
Exception? exception = null;
313313

@@ -346,7 +346,7 @@ static void DisposeFrame(IEnumerator? collectionEnumerator, ref Exception? excep
346346
/// Walks the stack cleaning up any leftover I(Async)Disposables
347347
/// in the event of an exception on async serialization
348348
/// </summary>
349-
public async ValueTask DisposePendingDisposablesOnExceptionAsync()
349+
public readonly async ValueTask DisposePendingDisposablesOnExceptionAsync()
350350
{
351351
Exception? exception = null;
352352

src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -586,9 +586,9 @@ public static void ThrowNotSupportedException(scoped ref ReadStack state, in Utf
586586
}
587587

588588
[DoesNotReturn]
589-
public static void ThrowNotSupportedException(ref WriteStack state, NotSupportedException ex)
589+
public static void ThrowNotSupportedException(ref WriteStack state, Exception innerException)
590590
{
591-
string message = ex.Message;
591+
string message = innerException.Message;
592592

593593
// The caller should check to ensure path is not already set.
594594
Debug.Assert(!message.Contains(" Path: "));
@@ -608,7 +608,7 @@ public static void ThrowNotSupportedException(ref WriteStack state, NotSupported
608608

609609
message += $" Path: {state.PropertyPath()}.";
610610

611-
throw new NotSupportedException(message, ex);
611+
throw new NotSupportedException(message, innerException);
612612
}
613613

614614
[DoesNotReturn]

src/libraries/System.Text.Json/tests/Common/CollectionTests/CollectionTests.Generic.Write.cs

+28
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,34 @@ public async Task WriteISetT_DisposesEnumerators()
955955
}
956956
}
957957

958+
[Fact]
959+
public async Task WriteIEnumerableT_ElementSerializationThrows_DisposesEnumerators()
960+
{
961+
var items = new RefCountedList<IEnumerable<int>>(Enumerable.Repeat(ThrowingEnumerable(), 1));
962+
await Assert.ThrowsAsync<DivideByZeroException>(() => Serializer.SerializeWrapper(items.AsEnumerable()));
963+
Assert.Equal(0, items.RefCount);
964+
965+
static IEnumerable<int> ThrowingEnumerable()
966+
{
967+
yield return 42;
968+
throw new DivideByZeroException();
969+
}
970+
}
971+
972+
[Fact]
973+
public async Task WriteIDictionaryT_ElementSerializationThrows_DisposesEnumerators()
974+
{
975+
var items = new RefCountedDictionary<int, IEnumerable<int>>(Enumerable.Repeat(new KeyValuePair<int, IEnumerable<int>>(42, ThrowingEnumerable()), 1));
976+
await Assert.ThrowsAsync<DivideByZeroException>(() => Serializer.SerializeWrapper((IDictionary<int, IEnumerable<int>>)items));
977+
Assert.Equal(0, items.RefCount);
978+
979+
static IEnumerable<int> ThrowingEnumerable()
980+
{
981+
yield return 42;
982+
throw new DivideByZeroException();
983+
}
984+
}
985+
958986
public class SimpleClassWithKeyValuePairs
959987
{
960988
public KeyValuePair<string, string> KvpWStrVal { get; set; }

src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/CollectionTests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public async Task DeserializeAsyncEnumerable()
130130
[JsonSerializable(typeof(JsonElement))]
131131
[JsonSerializable(typeof(string))]
132132
[JsonSerializable(typeof(IDictionary<int, int>))]
133+
[JsonSerializable(typeof(IDictionary<int, IEnumerable<int>>))]
133134
[JsonSerializable(typeof(Dictionary<string, ClassWithInternalParameterlessConstructor>))]
134135
[JsonSerializable(typeof(Dictionary<string, ClassWithPrivateParameterlessConstructor>))]
135136
[JsonSerializable(typeof(Dictionary<string, Dictionary<string, CustomClass>>))]
@@ -556,6 +557,7 @@ public CollectionTests_Default()
556557
[JsonSerializable(typeof(JsonElement))]
557558
[JsonSerializable(typeof(string))]
558559
[JsonSerializable(typeof(IDictionary<int, int>))]
560+
[JsonSerializable(typeof(IDictionary<int, IEnumerable<int>>))]
559561
[JsonSerializable(typeof(Dictionary<string, ClassWithInternalParameterlessConstructor>))]
560562
[JsonSerializable(typeof(Dictionary<string, ClassWithPrivateParameterlessConstructor>))]
561563
[JsonSerializable(typeof(Dictionary<string, Dictionary<string, CustomClass>>))]

0 commit comments

Comments
 (0)