diff --git a/Src/Newtonsoft.Json.Tests/Issues/Issue2694.cs b/Src/Newtonsoft.Json.Tests/Issues/Issue2694.cs new file mode 100644 index 000000000..fbb1a947e --- /dev/null +++ b/Src/Newtonsoft.Json.Tests/Issues/Issue2694.cs @@ -0,0 +1,158 @@ +#region License +// Copyright (c) 2007 James Newton-King +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +#endregion + +#if !(NET20 || NET35 || NET40 || PORTABLE || PORTABLE40) || NETSTANDARD1_3 || NETSTANDARD2_0 || NET6_0_OR_GREATER +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +#if DNXCORE50 +using System.Reflection; +using Xunit; +using Test = Xunit.FactAttribute; +using Assert = Newtonsoft.Json.Tests.XUnitAssert; +#else +using NUnit.Framework; +#endif + +namespace Newtonsoft.Json.Tests.Issues +{ + [TestFixture] + public class Issue2694 : TestFixtureBase + { +#if NET6_0_OR_GREATER + [Test] + public async Task Test_Reader_DisposeAsync() + { + JsonTextReader reader = new JsonTextReader(new StringReader("{}")); + IAsyncDisposable asyncDisposable = reader; + await asyncDisposable.DisposeAsync(); + + await ExceptionAssert.ThrowsAsync(() => reader.ReadAsync(), + "Unexpected state: Closed. Path '', line 1, position 0."); + } + + [Test] + public async Task Test_Writer_DisposeAsync() + { + MemoryStream ms = new MemoryStream(); + Stream s = new AsyncOnlyStream(ms); + StreamWriter sr = new StreamWriter(s, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 2, leaveOpen: true); + await using (JsonTextWriter writer = new JsonTextWriter(sr)) + { + await writer.WriteStartObjectAsync(); + } + + string json = Encoding.UTF8.GetString(ms.ToArray()); + Assert.AreEqual("{}", json); + } +#endif + + [Test] + public async Task Test_Writer_CloseAsync() + { + MemoryStream ms = new MemoryStream(); + Stream s = new AsyncOnlyStream(ms); + StreamWriter sr = new StreamWriter(s, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), 2, leaveOpen: true); + JsonTextWriter writer = new JsonTextWriter(sr); + await writer.WriteStartObjectAsync(); + + await writer.CloseAsync(); + + string json = Encoding.UTF8.GetString(ms.ToArray()); + Assert.AreEqual("{}", json); + } + + public class AsyncOnlyStream : Stream + { + private readonly Stream _innerStream; + private int _unflushedContentLength; + + public AsyncOnlyStream(Stream innerStream) + { + _innerStream = innerStream; + } + + public override void Flush() + { + // It's ok to call Flush if the content was already processed with FlushAsync. + if (_unflushedContentLength > 0) + { + throw new Exception($"Flush when there is {_unflushedContentLength} bytes buffered."); + } + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + _unflushedContentLength = 0; + return _innerStream.FlushAsync(cancellationToken); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + _unflushedContentLength += count; + return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override bool CanRead => _innerStream.CanRead; + public override bool CanSeek => _innerStream.CanSeek; + public override bool CanWrite => _innerStream.CanWrite; + public override long Length => _innerStream.Length; + + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + } + } +} +#endif \ No newline at end of file diff --git a/Src/Newtonsoft.Json/JsonReader.Async.cs b/Src/Newtonsoft.Json/JsonReader.Async.cs index 024903e76..537674f19 100644 --- a/Src/Newtonsoft.Json/JsonReader.Async.cs +++ b/Src/Newtonsoft.Json/JsonReader.Async.cs @@ -34,7 +34,25 @@ namespace Newtonsoft.Json { public abstract partial class JsonReader +#if HAVE_ASYNC_DISPOABLE + : IAsyncDisposable +#endif { +#if HAVE_ASYNC_DISPOABLE + ValueTask IAsyncDisposable.DisposeAsync() + { + try + { + Dispose(true); + return default; + } + catch (Exception exc) + { + return ValueTask.FromException(exc); + } + } +#endif + /// /// Asynchronously reads the next JSON token from the source. /// diff --git a/Src/Newtonsoft.Json/JsonTextWriter.Async.cs b/Src/Newtonsoft.Json/JsonTextWriter.Async.cs index f3ac2398d..b13b6e4c9 100644 --- a/Src/Newtonsoft.Json/JsonTextWriter.Async.cs +++ b/Src/Newtonsoft.Json/JsonTextWriter.Async.cs @@ -132,7 +132,34 @@ internal async Task DoCloseAsync(CancellationToken cancellationToken) await WriteEndAsync(cancellationToken).ConfigureAwait(false); } - CloseBufferAndWriter(); + await CloseBufferAndWriterAsync().ConfigureAwait(false); + } + + private async Task CloseBufferAndWriterAsync() + { + if (_writeBuffer != null) + { + BufferUtils.ReturnBuffer(_arrayPool, _writeBuffer); + _writeBuffer = null; + } + + if (CloseOutput && _writer != null) + { +#if HAVE_ASYNC_DISPOABLE + await _writer.DisposeAsync().ConfigureAwait(false); +#else + // DisposeAsync isn't available. Instead, flush any remaining content with FlushAsync + // to prevent Close/Dispose from making a blocking flush. + // + // No cancellation token on TextWriter.FlushAsync?! + await _writer.FlushAsync().ConfigureAwait(false); +#if HAVE_STREAM_READER_WRITER_CLOSE + _writer.Close(); +#else + _writer.Dispose(); +#endif +#endif + } } /// diff --git a/Src/Newtonsoft.Json/JsonWriter.Async.cs b/Src/Newtonsoft.Json/JsonWriter.Async.cs index 4610594c3..acb7170a3 100644 --- a/Src/Newtonsoft.Json/JsonWriter.Async.cs +++ b/Src/Newtonsoft.Json/JsonWriter.Async.cs @@ -37,7 +37,20 @@ namespace Newtonsoft.Json { public abstract partial class JsonWriter +#if HAVE_ASYNC_DISPOABLE + : IAsyncDisposable +#endif { +#if HAVE_ASYNC_DISPOABLE + async ValueTask IAsyncDisposable.DisposeAsync() + { + if (_currentState != State.Closed) + { + await CloseAsync().ConfigureAwait(false); + } + } +#endif + internal Task AutoCompleteAsync(JsonToken tokenBeingWritten, CancellationToken cancellationToken) { State oldState = _currentState; diff --git a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj index 60fe7e82a..d08c33e84 100644 --- a/Src/Newtonsoft.Json/Newtonsoft.Json.csproj +++ b/Src/Newtonsoft.Json/Newtonsoft.Json.csproj @@ -53,7 +53,7 @@ Json.NET .NET 6.0 - HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_ASYNC;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_METHOD_IMPL_ATTRIBUTE;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_READ_ONLY_COLLECTIONS;HAVE_REFLECTION_EMIT;HAVE_REGEX_TIMEOUTS;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;HAVE_INDEXOF_STRING_COMPARISON;HAVE_REPLACE_STRING_COMPARISON;HAVE_REPLACE_STRING_COMPARISON;HAVE_GETHASHCODE_STRING_COMPARISON;HAVE_NULLABLE_ATTRIBUTES;HAVE_DYNAMIC_CODE_COMPILED;HAS_ARRAY_EMPTY;HAVE_DATE_ONLY;$(AdditionalConstants) + HAVE_ADO_NET;HAVE_APP_DOMAIN;HAVE_ASYNC;HAVE_ASYNC_DISPOABLE;HAVE_BIG_INTEGER;HAVE_BINARY_FORMATTER;HAVE_BINARY_SERIALIZATION;HAVE_BINARY_EXCEPTION_SERIALIZATION;HAVE_CHAR_TO_LOWER_WITH_CULTURE;HAVE_CHAR_TO_STRING_WITH_CULTURE;HAVE_COM_ATTRIBUTES;HAVE_COMPONENT_MODEL;HAVE_CONCURRENT_COLLECTIONS;HAVE_COVARIANT_GENERICS;HAVE_DATA_CONTRACTS;HAVE_DATE_TIME_OFFSET;HAVE_DB_NULL_TYPE_CODE;HAVE_DYNAMIC;HAVE_EMPTY_TYPES;HAVE_ENTITY_FRAMEWORK;HAVE_EXPRESSIONS;HAVE_FAST_REVERSE;HAVE_FSHARP_TYPES;HAVE_FULL_REFLECTION;HAVE_GUID_TRY_PARSE;HAVE_HASH_SET;HAVE_ICLONEABLE;HAVE_ICONVERTIBLE;HAVE_IGNORE_DATA_MEMBER_ATTRIBUTE;HAVE_INOTIFY_COLLECTION_CHANGED;HAVE_INOTIFY_PROPERTY_CHANGING;HAVE_ISET;HAVE_LINQ;HAVE_MEMORY_BARRIER;HAVE_METHOD_IMPL_ATTRIBUTE;HAVE_NON_SERIALIZED_ATTRIBUTE;HAVE_READ_ONLY_COLLECTIONS;HAVE_REFLECTION_EMIT;HAVE_REGEX_TIMEOUTS;HAVE_SECURITY_SAFE_CRITICAL_ATTRIBUTE;HAVE_SERIALIZATION_BINDER_BIND_TO_NAME;HAVE_STREAM_READER_WRITER_CLOSE;HAVE_STRING_JOIN_WITH_ENUMERABLE;HAVE_TIME_SPAN_PARSE_WITH_CULTURE;HAVE_TIME_SPAN_TO_STRING_WITH_CULTURE;HAVE_TIME_ZONE_INFO;HAVE_TRACE_WRITER;HAVE_TYPE_DESCRIPTOR;HAVE_UNICODE_SURROGATE_DETECTION;HAVE_VARIANT_TYPE_PARAMETERS;HAVE_VERSION_TRY_PARSE;HAVE_XLINQ;HAVE_XML_DOCUMENT;HAVE_XML_DOCUMENT_TYPE;HAVE_CONCURRENT_DICTIONARY;HAVE_INDEXOF_STRING_COMPARISON;HAVE_REPLACE_STRING_COMPARISON;HAVE_REPLACE_STRING_COMPARISON;HAVE_GETHASHCODE_STRING_COMPARISON;HAVE_NULLABLE_ATTRIBUTES;HAVE_DYNAMIC_CODE_COMPILED;HAS_ARRAY_EMPTY;HAVE_DATE_ONLY;$(AdditionalConstants) Json.NET .NET 4.5