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()) {