diff --git a/Directory.Packages.props b/Directory.Packages.props
index 8868280..5c09f2f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,7 +2,6 @@
true
-
@@ -15,8 +14,8 @@
-
-
+
+
diff --git a/Gress.Tests/Gress.Tests.csproj b/Gress.Tests/Gress.Tests.csproj
index 5290706..dab483a 100644
--- a/Gress.Tests/Gress.Tests.csproj
+++ b/Gress.Tests/Gress.Tests.csproj
@@ -11,6 +11,7 @@
+
diff --git a/Gress.Tests/IntegrationSpecs.cs b/Gress.Tests/IntegrationSpecs.cs
new file mode 100644
index 0000000..65fca69
--- /dev/null
+++ b/Gress.Tests/IntegrationSpecs.cs
@@ -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.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();
+
+ // 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();
+
+ // 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();
+
+ // 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();
+
+ // 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
+ );
+
+ // Assert
+ new FileInfo(tempFile.Path)
+ .Length.Should()
+ .BeGreaterThan(0);
+
+ progress.GetValues().Should().NotBeEmpty();
+ }
+}
diff --git a/Gress/Integration/HttpClientExtensions.cs b/Gress/Integration/HttpClientExtensions.cs
new file mode 100644
index 0000000..67d94bd
--- /dev/null
+++ b/Gress/Integration/HttpClientExtensions.cs
@@ -0,0 +1,120 @@
+using System;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using PowerKit.Extensions;
+
+namespace Gress.Integration;
+
+///
+/// Provides progress-aware extensions for .
+///
+public static class HttpClientExtensions
+{
+ ///
+ extension(HttpClient client)
+ {
+ ///
+ /// Sends a GET request and returns the response body as a byte array.
+ ///
+ public async Task GetByteArrayAsync(
+ Uri requestUri,
+ IProgress? 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);
+ }
+
+ ///
+ /// Sends a GET request and returns the response body as a byte array.
+ ///
+ public async Task GetByteArrayAsync(
+ string requestUri,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await client
+ .GetByteArrayAsync(
+ new Uri(requestUri, UriKind.RelativeOrAbsolute),
+ progress,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ ///
+ /// Sends a GET request and returns the response body as text.
+ ///
+ public async Task GetStringAsync(
+ Uri requestUri,
+ IProgress? 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);
+ }
+
+ ///
+ /// Sends a GET request and returns the response body as text.
+ ///
+ public async Task GetStringAsync(
+ string requestUri,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await client
+ .GetStringAsync(
+ new Uri(requestUri, UriKind.RelativeOrAbsolute),
+ progress,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ ///
+ /// Sends a GET request and saves the response body to a file.
+ ///
+ public async Task DownloadAsync(
+ Uri requestUri,
+ string filePath,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await client
+ .DownloadAsync(requestUri, filePath, progress?.ToDoubleBased(), cancellationToken)
+ .ConfigureAwait(false);
+
+ ///
+ /// Sends a GET request and saves the response body to a file.
+ ///
+ public async Task DownloadAsync(
+ string requestUri,
+ string filePath,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await client
+ .DownloadAsync(
+ new Uri(requestUri, UriKind.RelativeOrAbsolute),
+ filePath,
+ progress,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+}
diff --git a/Gress/Integration/HttpContentExtensions.cs b/Gress/Integration/HttpContentExtensions.cs
new file mode 100644
index 0000000..b7dcd95
--- /dev/null
+++ b/Gress/Integration/HttpContentExtensions.cs
@@ -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;
+
+///
+/// Provides progress-aware extensions for .
+///
+public static class HttpContentExtensions
+{
+ ///
+ extension(HttpContent content)
+ {
+ ///
+ /// Serializes the HTTP content and writes it to the specified stream.
+ ///
+ public async Task CopyToAsync(
+ Stream destination,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await content
+ .CopyToStreamAsync(destination, progress?.ToDoubleBased(), cancellationToken)
+ .ConfigureAwait(false);
+
+ private async Task ReadAsMemoryStreamAsync(
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var destination = new MemoryStream();
+
+ await content
+ .CopyToAsync(destination, progress, cancellationToken)
+ .ConfigureAwait(false);
+
+ destination.Position = 0;
+
+ return destination;
+ }
+
+ ///
+ /// Reads the HTTP content and returns it as a byte array.
+ ///
+ public async Task ReadAsByteArrayAsync(
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ )
+ {
+ using var buffer = await content
+ .ReadAsMemoryStreamAsync(progress, cancellationToken)
+ .ConfigureAwait(false);
+
+ return buffer.ToArray();
+ }
+
+ ///
+ /// Reads the HTTP content and returns it as a string.
+ ///
+ public async Task ReadAsStringAsync(
+ IProgress? 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);
+ }
+ }
+}
diff --git a/Gress/Integration/StreamExtensions.cs b/Gress/Integration/StreamExtensions.cs
new file mode 100644
index 0000000..fc1875c
--- /dev/null
+++ b/Gress/Integration/StreamExtensions.cs
@@ -0,0 +1,52 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using PowerKit.Extensions;
+
+namespace Gress.Integration;
+
+///
+/// Provides progress-aware extensions for .
+///
+public static class StreamExtensions
+{
+ ///
+ extension(Stream source)
+ {
+ ///
+ /// Asynchronously copies bytes from the source stream to the destination stream.
+ ///
+ public async Task CopyToAsync(
+ Stream destination,
+ long sourceLength,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await source
+ .CopyToAsync(
+ destination,
+ sourceLength,
+ progress?.ToDoubleBased(),
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+
+ ///
+ /// Asynchronously copies bytes from the source stream to the destination stream.
+ ///
+ public async Task CopyToAsync(
+ Stream destination,
+ IProgress? progress,
+ CancellationToken cancellationToken = default
+ ) =>
+ await source
+ .CopyToAsync(
+ destination,
+ source.CanSeek ? source.Length : -1,
+ progress,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ }
+}
diff --git a/Readme.md b/Readme.md
index b80fdb8..2a1b8b4 100644
--- a/Readme.md
+++ b/Readme.md
@@ -538,3 +538,27 @@ subProgress3.Report(Percentage.FromFraction(0.5));
> [!NOTE]
> You can wrap an instance of `ICompletableProgress` in a disposable container by calling `ToDisposable()`.
> This allows you to place the handler in a `using (...)` block, which ensures that the completion is always reported at the end.
+
+### Integration extensions
+
+**Gress** also provides extensions for several built-in .NET types that integrate `IProgress` into their existing APIs.
+For example, you can use the below `Stream.CopyToAsync(...)` overload to copy data between two streams while tracking the operation's progress:
+
+```csharp
+using Gress;
+using Gress.Integration;
+
+await using var source = File.OpenRead("input.bin");
+await using var destination = File.Create("output.bin");
+
+await source.CopyToAsync(
+ destination,
+ new Progress(p => Console.WriteLine($"Copied: {p}"))
+);
+
+// Console output:
+// Copied: 37,5%
+// Copied: 62,5%
+// Copied: 87,5%
+// Copied: 100,0%
+```