diff --git a/src/ModularPipelines/Extensions/StreamExtensions.cs b/src/ModularPipelines/Extensions/StreamExtensions.cs
index 03104816f1..9e28c67532 100644
--- a/src/ModularPipelines/Extensions/StreamExtensions.cs
+++ b/src/ModularPipelines/Extensions/StreamExtensions.cs
@@ -8,16 +8,44 @@ public static class StreamExtensions
///
/// Turns a generic into a .
///
+ ///
+ ///
+ /// Breaking change: This method previously always disposed the source stream.
+ /// It now defaults to disposeSource: false to follow the principle of least surprise.
+ /// Existing callers that relied on automatic disposal should pass disposeSource: true.
+ ///
+ ///
+ /// When the input is already a and is false,
+ /// the same instance is returned (with position reset) as an optimization.
+ /// When is true, a copy is made before disposing the source.
+ ///
+ ///
+ /// Callers are responsible for disposing the returned .
+ ///
+ ///
/// Any stream.
- /// A MemoryStream containing the Stream's data.
- public static async Task ToMemoryStreamAsync(this Stream stream)
+ ///
+ /// When true, disposes the source stream after copying.
+ /// Defaults to false - callers are responsible for disposing the source stream.
+ ///
+ /// A MemoryStream containing the Stream's data, with Position set to 0 for reading.
+ public static async Task ToMemoryStreamAsync(this Stream stream, bool disposeSource = false)
{
- if (stream is MemoryStream memoryStream)
+ // If input is already a MemoryStream and we don't need to dispose it,
+ // return it directly (optimization to avoid unnecessary copy)
+ if (stream is MemoryStream sourceMs && !disposeSource)
{
- return memoryStream;
+ if (sourceMs.CanSeek)
+ {
+ sourceMs.Position = 0;
+ }
+
+ return sourceMs;
}
- memoryStream = new MemoryStream();
+ // Copy to new MemoryStream (handles both non-MemoryStream inputs
+ // and MemoryStream inputs when disposeSource is true)
+ var memoryStream = new MemoryStream();
if (stream.CanSeek)
{
@@ -26,8 +54,12 @@ public static async Task ToMemoryStreamAsync(this Stream stream)
await stream.CopyToAsync(memoryStream).ConfigureAwait(false);
- await stream.DisposeAsync().ConfigureAwait(false);
+ if (disposeSource)
+ {
+ await stream.DisposeAsync().ConfigureAwait(false);
+ }
+ memoryStream.Position = 0;
return memoryStream;
}
}
\ No newline at end of file
diff --git a/test/ModularPipelines.UnitTests/FileTests.cs b/test/ModularPipelines.UnitTests/FileTests.cs
index 91979e3088..c99467362b 100644
--- a/test/ModularPipelines.UnitTests/FileTests.cs
+++ b/test/ModularPipelines.UnitTests/FileTests.cs
@@ -107,7 +107,7 @@ public async Task ReadEmptyFile()
var plainText = await file.ReadAsync();
var lines = await ToListAsync(file.ReadLinesAsync());
var bytes = await file.ReadBytesAsync();
- var stream = await file.GetStream().ToMemoryStreamAsync();
+ await using var stream = await file.GetStream().ToMemoryStreamAsync(disposeSource: true);
using (Assert.Multiple())
{
@@ -128,7 +128,7 @@ public async Task ReadWriteFile()
var plainText = await file.ReadAsync();
var lines = await ToListAsync(file.ReadLinesAsync());
var bytes = await file.ReadBytesAsync();
- var stream = await file.GetStream().ToMemoryStreamAsync();
+ await using var stream = await file.GetStream().ToMemoryStreamAsync(disposeSource: true);
using (Assert.Multiple())
{