Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
80b1a53
Add ToMemoryStream and ToMemoryStreamAsync extensions with tests
Copilot Jul 20, 2026
9319ccf
Fix test coverage and clarify docs for ToMemoryStream fast-path
Copilot Jul 20, 2026
c066a9c
Address code review feedback: remarks section and explicit CopyToAsyn…
Copilot Jul 20, 2026
107ffcd
Re-architect stream materialization as a lazy MemoryBackedStream wrapper
Copilot Jul 20, 2026
98c2118
Address code review: clamp position, document Flush no-op, truncate o…
Copilot Jul 20, 2026
fa8f4c4
Merge remote-tracking branch 'origin/prime' into copilot/add-to-memor…
Copilot Jul 20, 2026
e0a459b
Simplify EnsureBuffer, delegate Flush, replace stream wrappers with f…
Copilot Jul 21, 2026
e2caac0
Merge branch 'prime' into copilot/add-to-memory-stream-extension
Tyrrrz Jul 22, 2026
c1451ea
Make MemoryBackedStream a public class instead of an extension method
Copilot Jul 22, 2026
974d6e6
Retain source's initial position in buffer when buffering a seekable …
Copilot Jul 22, 2026
16270b3
Treat all source streams as unseekable in MemoryBackedStream
Copilot Jul 23, 2026
1b4499e
Document trailing-data behavior and add write-back position test
Copilot Jul 23, 2026
4f0c450
Split MemoryBackedStream into MemoryReadStream and MemoryWriteStream
Copilot Jul 23, 2026
1c0883f
MemoryWriteStream: flush to source on Flush() instead of Dispose()
Copilot Jul 23, 2026
a064ed6
Consolidate stream tests to one focused test each
Copilot Jul 23, 2026
3804c2e
Fix MemoryReadStream test to use a genuinely non-seekable source
Copilot Jul 23, 2026
4dc8071
Extract NonSeekableStream as shared test utility; use in both read an…
Copilot Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions PowerKit.Tests/Extensions/StreamExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
Expand All @@ -10,6 +11,93 @@ namespace PowerKit.Tests.Extensions;

public class StreamExtensionsTests
{
// A non-MemoryStream wrapper used to exercise the copy path
private sealed class NonMemoryStream(Stream inner) : Stream
{
public override bool CanRead => inner.CanRead;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}

public override void Flush() => inner.Flush();

public override int Read(byte[] buffer, int offset, int count) =>
inner.Read(buffer, offset, count);

public override long Seek(long offset, SeekOrigin origin) =>
throw new NotSupportedException();

public override void SetLength(long value) => throw new NotSupportedException();

public override void Write(byte[] buffer, int offset, int count) =>
throw new NotSupportedException();
}
Comment thread
Tyrrrz marked this conversation as resolved.
Outdated

[Fact]
public void ToMemoryStream_RegularStream_Test()
{
// Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
using var inner = new MemoryStream(data);
using var source = new NonMemoryStream(inner);

// Act
using var result = source.ToMemoryStream();

// Assert
result.Position.Should().Be(0);
result.ToArray().Should().Equal(data);
}

[Fact]
public void ToMemoryStream_AlreadyMemoryStream_ReturnsSameInstance_Test()
{
// Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
using var source = new MemoryStream(data);

// Act
var result = source.ToMemoryStream();

// Assert
result.Should().BeSameAs(source);
}

[Fact]
public async Task ToMemoryStreamAsync_RegularStream_Test()
{
// Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
using var inner = new MemoryStream(data);
using var source = new NonMemoryStream(inner);

// Act
using var result = await source.ToMemoryStreamAsync();

// Assert
result.Position.Should().Be(0);
result.ToArray().Should().Equal(data);
}

[Fact]
public async Task ToMemoryStreamAsync_AlreadyMemoryStream_ReturnsSameInstance_Test()
{
// Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
using var source = new MemoryStream(data);

// Act
var result = await source.ToMemoryStreamAsync();

// Assert
result.Should().BeSameAs(source);
}

[Fact]
public async Task CopyToAsync_AutoFlush_Test()
{
Expand Down
44 changes: 44 additions & 0 deletions PowerKit/Extensions/StreamExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,50 @@ public static class StreamExtensions
{
extension(Stream source)
{
/// <summary>
/// Copies the contents of the stream into a new <see cref="MemoryStream" /> with
/// <see cref="Stream.Position" /> reset to 0.
/// </summary>
Comment thread
Tyrrrz marked this conversation as resolved.
/// <remarks>
/// If the stream is already a <see cref="MemoryStream" />, it is returned as-is
/// without resetting its position.
/// </remarks>
public MemoryStream ToMemoryStream()
{
if (source is MemoryStream asMemoryStream)
return asMemoryStream;

var memoryStream = new MemoryStream();
source.CopyTo(memoryStream);
memoryStream.Position = 0;

return memoryStream;
}

#if NET40_OR_GREATER || NETSTANDARD || NET
/// <summary>
/// Copies the contents of the stream into a new <see cref="MemoryStream" /> asynchronously
/// with <see cref="Stream.Position" /> reset to 0.
/// </summary>
Comment thread
Tyrrrz marked this conversation as resolved.
/// <remarks>
/// If the stream is already a <see cref="MemoryStream" />, it is returned as-is
/// without resetting its position.
/// </remarks>
public async Task<MemoryStream> ToMemoryStreamAsync(
CancellationToken cancellationToken = default
)
{
if (source is MemoryStream asMemoryStream)
return asMemoryStream;

var memoryStream = new MemoryStream();
await source.CopyToAsync(memoryStream, 81920, cancellationToken).ConfigureAwait(false);
memoryStream.Position = 0;

return memoryStream;
}

#endif
#if NET40_OR_GREATER || NETSTANDARD || NET
/// <summary>
/// Copies the contents of the stream to the destination stream, optionally flushing after each write.
Expand Down