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..2055496 --- /dev/null +++ b/PolyShim/Net70/File.cs @@ -0,0 +1,79 @@ +#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) + { + cancellationToken.ThrowIfCancellationRequested(); + + 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) + { + cancellationToken.ThrowIfCancellationRequested(); + + 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