-
-
Notifications
You must be signed in to change notification settings - Fork 11
Add extensions for Stream, HttpContent, HttpClient that make use of progress reporting #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Tyrrrz
merged 42 commits into
prime
from
copilot/add-public-extensions-for-progress-reporting
Apr 20, 2026
+393
−3
Merged
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
1e6575e
feat: add Stream.CopyTo/CopyToAsync extensions with progress reporting
Copilot 498a0af
feat: add HttpContent.CopyToAsync extension and tests for Stream/Http…
Copilot 6f8f63c
fix: pass cancellationToken to ReadAsStreamAsync in HttpContentExtens…
Copilot fac8335
feat: add streamLength overloads to Stream.CopyTo and Stream.CopyToAsync
Copilot b21ba2e
feat: add ReadAsByteArrayAsync/ReadAsStringAsync to HttpContent and G…
Copilot 3abc455
Add DownloadAsync overloads to HttpClientExtensions
Copilot 6b65775
Move HTTP/stream extensions to Gress.Http namespace
Copilot 9d77be6
Add DownloadAsync file-path overloads to HttpClientExtensions
Copilot 16f3116
Use TempFile from PowerKit in file-download tests
Copilot 913e849
Replace FakeHttpMessageHandler with real HTTP requests to example.com
Copilot ced4b90
Extract TestUrl constant; use IClassFixture for shared HttpClient
Copilot 0026a3f
Remove null tests; only test string overloads; delegate string to Uri…
Copilot befdb88
Inline HttpClient in HttpClientSpecs, remove Fixture/IClassFixture
Copilot eff8a44
Inline HttpClient and URL directly into each test method
Copilot 86a1a83
Update HttpClientSpecs.cs
Tyrrrz e4330dc
Apply suggestion from @Copilot
Tyrrrz 684aa79
Remove HttpContentSpecs.cs — covered by HttpClient tests via delegation
Copilot 78dbcdd
Update Gress/Http/HttpClientExtensions.cs
Tyrrrz ad91080
Merge top 2 stream tests + HttpClient tests into ExtensionSpecs; remo…
Copilot 61353fe
Use UriKind.RelativeOrAbsolute in all string-overload Uri constructions
Copilot ce511e8
Remove unpredictable progress assertions from HttpClient tests hittin…
Copilot 71898ff
Use https://github.com/Tyrrrz/Gress in HttpClient tests; restore prog…
Copilot dc22050
Update HttpClientExtensions.cs
Tyrrrz b7c8cf4
Use ArrayPool and Random to fill stream test data
Copilot 04bed4e
Add using var, ConfigureAwait(false), FileStream async options, reord…
Copilot bdbb9f4
refactor: use RentOwner in tests, ArrayPool+pattern matching in Strea…
Copilot 3f15226
fix: use buffer.Length instead of DefaultBufferSize in ArrayPool read…
Copilot 92d00f8
Update HttpClientExtensions.cs
Tyrrrz 82f4366
Update PowerKit to v1.0.0 and fix ISpanOwner API usage
Copilot 185abd2
Use ISpanOwner.Span API from PowerKit v1.0.0 in ExtensionSpecs
Copilot ac81aae
Update HttpContentExtensions.cs
Tyrrrz f8137d0
Update HttpContentExtensions.cs
Tyrrrz ce898dd
Add await to string delegating overloads in HttpClientExtensions; use…
Copilot b356be9
asd
Tyrrrz 535d58c
asd
Tyrrrz e203d97
asd
Tyrrrz c0261e0
Add readme info
Tyrrrz c3c137f
Call into PowerKit more
Tyrrrz 197ff8d
Use web resources that report content length in tests
Tyrrrz ea78627
Boost coverage
Tyrrrz d7a888f
Refactor
Tyrrrz 6a95d2e
Improve correctness
Tyrrrz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Net.Http; | ||
| using System.Threading.Tasks; | ||
| using FluentAssertions; | ||
| using Gress.Integration; | ||
| using PowerKit; | ||
| using PowerKit.Extensions; | ||
| using Xunit; | ||
|
|
||
| namespace Gress.Tests; | ||
|
|
||
| public class IntegrationSpecs | ||
| { | ||
| [Fact] | ||
| public async Task I_can_copy_a_stream_to_another_stream_with_progress() | ||
| { | ||
| // Arrange | ||
| using var buffer = SpanPool<byte>.Shared.Rent( | ||
| // Longer buffer to ensure multiple progress reports | ||
| 81920 * 2 | ||
| + 1000 | ||
| ); | ||
|
|
||
| Random.Shared.NextBytes(buffer.Span); | ||
| var data = buffer.Span.ToArray(); | ||
|
|
||
| using var source = new MemoryStream(data); | ||
| using var destination = new MemoryStream(); | ||
| var progress = new ProgressCollector<Percentage>(); | ||
|
|
||
| // Act | ||
| await source.CopyToAsync(destination, progress); | ||
|
|
||
| // Assert | ||
| destination.ToArray().Should().Equal(data); | ||
| progress.GetValues().Should().NotBeEmpty(); | ||
| progress.GetValues().Should().BeInAscendingOrder(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task I_can_download_a_web_resource_as_a_byte_array_with_progress() | ||
| { | ||
| // Arrange | ||
| using var http = new HttpClient(); | ||
| var progress = new ProgressCollector<Percentage>(); | ||
|
|
||
| // Act | ||
| var result = await http.GetByteArrayAsync( | ||
| // Need something that reports content length | ||
| "https://github.com/Tyrrrz/CliWrap/releases/download/3.10.1/CliWrap.3.10.1.nupkg", | ||
| progress | ||
| ); | ||
|
|
||
| // Assert | ||
| result.Should().NotBeNullOrEmpty(); | ||
| progress.GetValues().Should().NotBeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task I_can_download_a_web_resource_as_a_string_with_progress() | ||
| { | ||
| // Arrange | ||
| using var http = new HttpClient(); | ||
| var progress = new ProgressCollector<Percentage>(); | ||
|
|
||
| // Act | ||
| var result = await http.GetStringAsync( | ||
| // Need something that reports content length | ||
| "https://github.com/Tyrrrz/CliWrap/releases/download/3.10.1/CliWrap.3.10.1.nupkg", | ||
| progress | ||
| ); | ||
|
|
||
| // Assert | ||
| result.Should().NotBeNullOrEmpty(); | ||
| progress.GetValues().Should().NotBeEmpty(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task I_can_download_a_web_resource_to_a_file_with_progress() | ||
| { | ||
| // Arrange | ||
| using var http = new HttpClient(); | ||
| using var tempFile = TempFile.Create(); | ||
| var progress = new ProgressCollector<Percentage>(); | ||
|
|
||
| // Act | ||
| await http.DownloadAsync( | ||
| // Need something that reports content length | ||
| "https://github.com/Tyrrrz/CliWrap/releases/download/3.10.1/CliWrap.3.10.1.nupkg", | ||
| tempFile.Path, | ||
| progress | ||
| ); | ||
|
Tyrrrz marked this conversation as resolved.
|
||
|
|
||
| // Assert | ||
| new FileInfo(tempFile.Path) | ||
| .Length.Should() | ||
| .BeGreaterThan(0); | ||
|
|
||
| progress.GetValues().Should().NotBeEmpty(); | ||
| } | ||
| } | ||
|
Tyrrrz marked this conversation as resolved.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| using System; | ||
| using System.Net.Http; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using PowerKit.Extensions; | ||
|
|
||
| namespace Gress.Integration; | ||
|
|
||
| /// <summary> | ||
| /// Provides progress-aware extensions for <see cref="HttpClient" />. | ||
| /// </summary> | ||
| public static class HttpClientExtensions | ||
| { | ||
| /// <inheritdoc cref="HttpClientExtensions" /> | ||
| extension(HttpClient client) | ||
| { | ||
| /// <summary> | ||
| /// Sends a GET request and returns the response body as a byte array. | ||
| /// </summary> | ||
| public async Task<byte[]> GetByteArrayAsync( | ||
| Uri requestUri, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| using var response = await client | ||
| .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| response.EnsureSuccessStatusCode(); | ||
|
|
||
| return await response | ||
| .Content.ReadAsByteArrayAsync(progress, cancellationToken) | ||
| .ConfigureAwait(false); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sends a GET request and returns the response body as a byte array. | ||
| /// </summary> | ||
| public async Task<byte[]> GetByteArrayAsync( | ||
| string requestUri, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await client | ||
| .GetByteArrayAsync( | ||
| new Uri(requestUri, UriKind.RelativeOrAbsolute), | ||
| progress, | ||
| cancellationToken | ||
| ) | ||
| .ConfigureAwait(false); | ||
|
|
||
| /// <summary> | ||
| /// Sends a GET request and returns the response body as text. | ||
| /// </summary> | ||
| public async Task<string> GetStringAsync( | ||
| Uri requestUri, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| using var response = await client | ||
| .GetAsync(requestUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| response.EnsureSuccessStatusCode(); | ||
|
|
||
| return await response | ||
| .Content.ReadAsStringAsync(progress, cancellationToken) | ||
| .ConfigureAwait(false); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sends a GET request and returns the response body as text. | ||
| /// </summary> | ||
| public async Task<string> GetStringAsync( | ||
| string requestUri, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await client | ||
| .GetStringAsync( | ||
| new Uri(requestUri, UriKind.RelativeOrAbsolute), | ||
| progress, | ||
| cancellationToken | ||
| ) | ||
| .ConfigureAwait(false); | ||
|
|
||
| /// <summary> | ||
| /// Sends a GET request and saves the response body to a file. | ||
| /// </summary> | ||
| public async Task DownloadAsync( | ||
| Uri requestUri, | ||
| string filePath, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await client | ||
| .DownloadAsync(requestUri, filePath, progress?.ToDoubleBased(), cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| /// <summary> | ||
| /// Sends a GET request and saves the response body to a file. | ||
| /// </summary> | ||
| public async Task DownloadAsync( | ||
| string requestUri, | ||
| string filePath, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await client | ||
| .DownloadAsync( | ||
| new Uri(requestUri, UriKind.RelativeOrAbsolute), | ||
| filePath, | ||
| progress, | ||
| cancellationToken | ||
| ) | ||
| .ConfigureAwait(false); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Linq; | ||
| using System.Net.Http; | ||
| using System.Text; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using PowerKit.Extensions; | ||
|
|
||
| namespace Gress.Integration; | ||
|
|
||
| /// <summary> | ||
| /// Provides progress-aware extensions for <see cref="HttpContent" />. | ||
| /// </summary> | ||
| public static class HttpContentExtensions | ||
| { | ||
| /// <inheritdoc cref="HttpContentExtensions" /> | ||
| extension(HttpContent content) | ||
| { | ||
| /// <summary> | ||
| /// Serializes the HTTP content and writes it to the specified stream. | ||
| /// </summary> | ||
| public async Task CopyToAsync( | ||
|
Tyrrrz marked this conversation as resolved.
|
||
| Stream destination, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await content | ||
| .CopyToStreamAsync(destination, progress?.ToDoubleBased(), cancellationToken) | ||
|
Tyrrrz marked this conversation as resolved.
|
||
| .ConfigureAwait(false); | ||
|
|
||
| private async Task<MemoryStream> ReadAsMemoryStreamAsync( | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| var destination = new MemoryStream(); | ||
|
|
||
| await content | ||
| .CopyToAsync(destination, progress, cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| destination.Position = 0; | ||
|
|
||
| return destination; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Reads the HTTP content and returns it as a byte array. | ||
| /// </summary> | ||
| public async Task<byte[]> ReadAsByteArrayAsync( | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| using var buffer = await content | ||
| .ReadAsMemoryStreamAsync(progress, cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| return buffer.ToArray(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Reads the HTTP content and returns it as a string. | ||
| /// </summary> | ||
| public async Task<string> ReadAsStringAsync( | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| var encoding = | ||
| content | ||
| .Headers.ContentType?.CharSet?.Pipe(c => | ||
| Encoding | ||
| .GetEncodings() | ||
| .FirstOrDefault(e => | ||
| string.Equals(e.Name, c, StringComparison.OrdinalIgnoreCase) | ||
| ) | ||
| ) | ||
| ?.GetEncoding() | ||
| ?? Encoding.UTF8; | ||
|
|
||
| using var buffer = await content | ||
| .ReadAsMemoryStreamAsync(progress, cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| using var reader = new StreamReader(buffer, encoding, true); | ||
|
|
||
| return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| using System; | ||
| using System.IO; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using PowerKit.Extensions; | ||
|
|
||
| namespace Gress.Integration; | ||
|
|
||
| /// <summary> | ||
| /// Provides progress-aware extensions for <see cref="Stream" />. | ||
| /// </summary> | ||
| public static class StreamExtensions | ||
| { | ||
| /// <inheritdoc cref="StreamExtensions" /> | ||
| extension(Stream source) | ||
| { | ||
| /// <summary> | ||
| /// Asynchronously copies bytes from the source stream to the destination stream. | ||
| /// </summary> | ||
| public async Task CopyToAsync( | ||
| Stream destination, | ||
| long sourceLength, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await source | ||
| .CopyToAsync( | ||
| destination, | ||
| sourceLength, | ||
| progress?.ToDoubleBased(), | ||
| cancellationToken | ||
| ) | ||
| .ConfigureAwait(false); | ||
|
|
||
| /// <summary> | ||
| /// Asynchronously copies bytes from the source stream to the destination stream. | ||
| /// </summary> | ||
| public async Task CopyToAsync( | ||
| Stream destination, | ||
| IProgress<Percentage>? progress, | ||
| CancellationToken cancellationToken = default | ||
| ) => | ||
| await source | ||
| .CopyToAsync( | ||
| destination, | ||
| source.CanSeek ? source.Length : -1, | ||
| progress, | ||
| cancellationToken | ||
| ) | ||
| .ConfigureAwait(false); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.