From 924378a46deac20136dcbea3b96046aceea72039 Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Wed, 3 Dec 2025 19:51:26 +0200 Subject: [PATCH 1/2] Add polyfills for `File.ReadLinesAsync(...)`, `TextReader.ReadLineAsync(CancellationToken)`, `TextReader.ReadToEndAsync(CancellationToken)` --- PolyShim.Tests/Net70/FileTests.cs | 41 +++++++++++++ PolyShim.Tests/Net70/TextReaderTests.cs | 77 +++++++++++++++++++++++++ PolyShim/Net70/File.cs | 75 ++++++++++++++++++++++++ PolyShim/Net70/TextReader.cs | 50 ++++++++++++++++ PolyShim/Signatures.md | 8 ++- 5 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 PolyShim.Tests/Net70/FileTests.cs create mode 100644 PolyShim.Tests/Net70/TextReaderTests.cs create mode 100644 PolyShim/Net70/File.cs create mode 100644 PolyShim/Net70/TextReader.cs diff --git a/PolyShim.Tests/Net70/FileTests.cs b/PolyShim.Tests/Net70/FileTests.cs new file mode 100644 index 0000000..a0aa359 --- /dev/null +++ b/PolyShim.Tests/Net70/FileTests.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net70; + +public class FileTests +{ + [Fact] + public async Task ReadLinesAsync_Test() + { + // Arrange + var linesToWrite = new[] { "Line 1", "Line 2", "Line 3" }; + var tempFilePath = Path.GetTempFileName(); + await File.WriteAllLinesAsync(tempFilePath, linesToWrite); + + try + { + // Act + var readLines = new List(); + await foreach (var line in File.ReadLinesAsync(tempFilePath)) + readLines.Add(line); + + // Assert + readLines.Should().Equal(linesToWrite); + } + finally + { + try + { + File.Delete(tempFilePath); + } + catch + { + // Ignore + } + } + } +} diff --git a/PolyShim.Tests/Net70/TextReaderTests.cs b/PolyShim.Tests/Net70/TextReaderTests.cs new file mode 100644 index 0000000..7a76115 --- /dev/null +++ b/PolyShim.Tests/Net70/TextReaderTests.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net70; + +public class TextReaderTests +{ + [Fact] + public async Task ReadLineAsync_Test() + { + // Arrange + using var stream = new MemoryStream("Line 1\nLine 2\nLine 3\n"u8.ToArray()); + using var reader = new StreamReader(stream); + + // Act + var line1 = await reader.ReadLineAsync(CancellationToken.None); + var line2 = await reader.ReadLineAsync(CancellationToken.None); + var line3 = await reader.ReadLineAsync(CancellationToken.None); + var line4 = await reader.ReadLineAsync(CancellationToken.None); + + // Assert + line1.Should().Be("Line 1"); + line2.Should().Be("Line 2"); + line3.Should().Be("Line 3"); + line4.Should().BeNull(); + } + + [Fact] + public async Task ReadLineAsync_Cancellation_Test() + { + // Arrange + using var stream = new MemoryStream("Line 1\nLine 2\nLine 3\n"u8.ToArray()); + using var reader = new StreamReader(stream); + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => + { + await reader.ReadLineAsync(cts.Token); + }); + } + + [Fact] + public async Task ReadToEndAsync_Test() + { + // Arrange + using var stream = new MemoryStream("Hello, World!"u8.ToArray()); + using var reader = new StreamReader(stream); + + // Act + var content = await reader.ReadToEndAsync(CancellationToken.None); + + // Assert + content.Should().Be("Hello, World!"); + } + + [Fact] + public async Task ReadToEndAsync_Cancellation_Test() + { + // Arrange + using var stream = new MemoryStream("Hello, World!"u8.ToArray()); + using var reader = new StreamReader(stream); + using var cts = new CancellationTokenSource(); + await cts.CancelAsync(); + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => + { + await reader.ReadToEndAsync(cts.Token); + }); + } +} diff --git a/PolyShim/Net70/File.cs b/PolyShim/Net70/File.cs new file mode 100644 index 0000000..0a097a8 --- /dev/null +++ b/PolyShim/Net70/File.cs @@ -0,0 +1,75 @@ +#if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; + +internal static partial class PolyfillExtensions +{ + // No file I/O on .NET Standard prior to 1.3 +#if !NETSTANDARD || NETSTANDARD1_3_OR_GREATER + extension(File) + { +#if FEATURE_TASK && FEATURE_ASYNCINTERFACES + // https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-text-encoding-system-threading-cancellationtoken) + public static async IAsyncEnumerable ReadLinesAsync( + string path, + Encoding encoding, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + using var stream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan + ); + + using var reader = new StreamReader(stream, encoding); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (line is not null) + yield return line; + } + } + + // https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-threading-cancellationtoken) + public static async IAsyncEnumerable ReadLinesAsync( + string path, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + using var stream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan + ); + + using var reader = new StreamReader(stream); + + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (line is not null) + yield return line; + } + } +#endif + } +#endif +} +#endif diff --git a/PolyShim/Net70/TextReader.cs b/PolyShim/Net70/TextReader.cs new file mode 100644 index 0000000..08b960c --- /dev/null +++ b/PolyShim/Net70/TextReader.cs @@ -0,0 +1,50 @@ +#if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +internal static partial class PolyfillExtensions +{ + extension(TextReader reader) + { +#if FEATURE_TASK + // https://learn.microsoft.com/dotnet/api/system.io.textreader.readlineasync#system-io-textreader-readlineasync(system-threading-cancellationtoken) + public Task ReadLineAsync(CancellationToken cancellationToken) + { + // Impossible to polyfill this properly as it requires to track the buffer's state + cancellationToken.ThrowIfCancellationRequested(); + return reader.ReadLineAsync(); + } + + // https://learn.microsoft.com/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readtoendasync(system-threading-cancellationtoken) + public async Task ReadToEndAsync(CancellationToken cancellationToken) + { + var result = new StringBuilder(); + var buffer = new char[4096]; + + while (true) + { + var charsRead = await reader + .ReadAsync(buffer, cancellationToken) + .ConfigureAwait(false); + + if (charsRead <= 0) + break; + + result.Append(buffer, 0, charsRead); + } + + return result.ToString(); + } +#endif + } +} +#endif diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 7e78045..2f31ef7 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 335 +- **Total:** 339 - **Types:** 74 -- **Members:** 261 +- **Members:** 265 ___ @@ -89,6 +89,8 @@ ___ - `FeatureSwitchDefinitionAttribute` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.featureswitchdefinitionattribute) .NET 9.0 - `File` + - [`IAsyncEnumerable ReadLinesAsync(string, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-threading-cancellationtoken)) .NET 7.0 + - [`IAsyncEnumerable ReadLinesAsync(string, Encoding, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.readlinesasync#system-io-file-readlinesasync(system-string-system-text-encoding-system-threading-cancellationtoken)) .NET 7.0 - [`Task AppendAllBytesAsync(string, byte[], CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.appendallbytesasync#system-io-file-appendallbytesasync(system-string-system-byte()-system-threading-cancellationtoken)) .NET 9.0 - [`Task AppendAllLinesAsync(string, IEnumerable, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.appendalllinesasync#system-io-file-appendalllinesasync(system-string-system-collections-generic-ienumerable((system-string))-system-threading-cancellationtoken)) .NET Core 2.0 - [`Task AppendAllLinesAsync(string, IEnumerable, Encoding, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.file.appendalllinesasync#system-io-file-appendalllinesasync(system-string-system-collections-generic-ienumerable((system-string))-system-text-encoding-system-threading-cancellationtoken)) .NET Core 2.0 @@ -413,6 +415,8 @@ ___ - `TextReader` - [`int Read(Span)`](https://learn.microsoft.com/dotnet/api/system.io.textreader.read#system-io-textreader-read(system-span((system-char)))) .NET Core 2.1 - [`Task ReadAsync(Memory, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.textreader.readasync#system-io-textreader-readasync(system-memory((system-char))-system-threading-cancellationtoken)) .NET Core 2.1 + - [`Task ReadLineAsync(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.textreader.readlineasync#system-io-textreader-readlineasync(system-threading-cancellationtoken)) .NET 7.0 + - [`Task ReadToEndAsync(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.textreader.readtoendasync#system-io-textreader-readtoendasync(system-threading-cancellationtoken)) .NET 7.0 - `TextWriter` - [`Task DisposeAsync()`](https://learn.microsoft.com/dotnet/api/system.io.textwriter.disposeasync) .NET Core 3.0 - [`Task WriteAsync(ReadOnlyMemory, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.io.textwriter.writeasync#system-io-textwriter-writeasync(system-readonlymemory((system-char))-system-threading-cancellationtoken)) .NET Core 2.1 From 2633619dff196f93876534964485004112de632b Mon Sep 17 00:00:00 2001 From: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:04:22 +0200 Subject: [PATCH 2/2] asd --- PolyShim/Net70/File.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PolyShim/Net70/File.cs b/PolyShim/Net70/File.cs index 0a097a8..2055496 100644 --- a/PolyShim/Net70/File.cs +++ b/PolyShim/Net70/File.cs @@ -38,6 +38,8 @@ public static async IAsyncEnumerable ReadLinesAsync( while (!reader.EndOfStream) { + cancellationToken.ThrowIfCancellationRequested(); + var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line is not null) yield return line; @@ -63,6 +65,8 @@ public static async IAsyncEnumerable ReadLinesAsync( while (!reader.EndOfStream) { + cancellationToken.ThrowIfCancellationRequested(); + var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line is not null) yield return line;