diff --git a/docs/core/compatibility/11.md b/docs/core/compatibility/11.md index 521cb77a3ea0e..258a8fb1caf94 100644 --- a/docs/core/compatibility/11.md +++ b/docs/core/compatibility/11.md @@ -29,6 +29,7 @@ See [Breaking changes in ASP.NET Core 11](/aspnet/core/breaking-changes/11/overv | [Environment.TickCount made consistent with Windows timeout behavior](core-libraries/11/environment-tickcount-windows-behavior.md) | Behavioral change | | [NamedPipeServerStream with PipeOptions.CurrentUserOnly tightens Unix socket file permissions](core-libraries/11/namedpipeserverstream-unix-permissions.md) | Behavioral change | | [Nullable.GetUnderlyingType throws for custom Type subclasses](core-libraries/11/nullable-getunderlyingtype-throws.md) | Behavioral change | +| [PackagePart.GetStream() returns a non-seekable stream for compressed parts in ReadWrite packages](core-libraries/11/packagepart-getstream-non-seekable.md) | Behavioral change | | [API obsoletions with non-default diagnostic IDs (.NET 11)](core-libraries/11/obsolete-apis.md) | Source incompatible | | [SafeFileHandle.IsAsync and FileStream.IsAsync accurately reflect non-blocking state on Unix](core-libraries/11/safefilehandle-isasync-unix.md) | Behavioral change | | [TAR-reading APIs verify header checksums when reading](core-libraries/11/tar-checksum-validation.md) | Behavioral change | diff --git a/docs/core/compatibility/core-libraries/11/packagepart-getstream-non-seekable.md b/docs/core/compatibility/core-libraries/11/packagepart-getstream-non-seekable.md new file mode 100644 index 0000000000000..791f686556568 --- /dev/null +++ b/docs/core/compatibility/core-libraries/11/packagepart-getstream-non-seekable.md @@ -0,0 +1,91 @@ +--- +title: "Breaking change: PackagePart.GetStream() returns a non-seekable stream for compressed parts in ReadWrite packages" +description: "Learn about the breaking change in .NET 11 where PackagePart.GetStream() returns a forward-only stream instead of a seekable MemoryStream for compressed parts of a ReadWrite package." +ms.date: 08/04/2026 +ai-usage: ai-assisted +--- +# PackagePart.GetStream() returns a non-seekable stream for compressed parts in ReadWrite packages + +When you open a with and then open a compressed part for reading that hasn't been modified in the current session, now returns a forward-only stream instead of a seekable . + +## Version introduced + +.NET 11 Preview 7 + +## Previous behavior + +Previously, when you opened a `Package` with `FileAccess.ReadWrite` (which maps internally to `ZipArchiveMode.Update`), opening a compressed part for reading returned a seekable `MemoryStream` that contained the fully decompressed entry content. + +```csharp +using Package package = Package.Open("file.docx", FileMode.Open, FileAccess.ReadWrite); +PackagePart part = package.GetPart(new Uri("/word/document.xml", UriKind.Relative)); + +// Returned a seekable MemoryStream. +using Stream stream = part.GetStream(FileMode.Open, FileAccess.Read); +Console.WriteLine(stream.CanSeek); // true +stream.Seek(0, SeekOrigin.Begin); // succeeded +Console.WriteLine(stream.Position); // 0 +``` + +## New behavior + +Starting in .NET 11, the same call returns a forward-only (non-seekable) stream for compressed parts that haven't been modified in the current session. + +```csharp +using Package package = Package.Open("file.docx", FileMode.Open, FileAccess.ReadWrite); +PackagePart part = package.GetPart(new Uri("/word/document.xml", UriKind.Relative)); + +// Returns a forward-only (non-seekable) stream. +using Stream stream = part.GetStream(FileMode.Open, FileAccess.Read); +Console.WriteLine(stream.CanSeek); // false +stream.Seek(0, SeekOrigin.Begin); // throws NotSupportedException +stream.Position = 0; // throws NotSupportedException +Console.WriteLine(stream.Length); // still works (reported from entry metadata) +``` + +All the following conditions must be true simultaneously for you to observe this change: + +- The package is opened with `FileAccess.ReadWrite` (`Package.Open(..., FileAccess.ReadWrite)`). +- The part is opened for reading only (`GetStream(FileMode.Open, FileAccess.Read)`). +- The part is compressed (`CompressionOption` other than `NotCompressed`). +- The part wasn't written or modified earlier in the same session. +- The consumer unconditionally seeks the stream or reads `Position`. + +The following scenarios aren't affected: + +- Read-only packages (`FileAccess.Read`): Their compressed part streams were already forward-only. +- Uncompressed (`Stored`) parts: They remain seekable. +- Parts modified earlier in the current session: They're served from an in-memory snapshot, so they remain seekable. +- Accessing `Stream.Length`. +- Forward-only consumers (the common case, such as `XmlReader`, `XDocument.Load`, the Open XML SDK, and `CopyTo`). +- Target frameworks earlier than .NET 11: The optimization is gated behind `NET11_0_OR_GREATER`. + +## Type of breaking change + +This change is a [behavioral change](../../categories.md#behavioral-change). + +## Reason for change + +Previously, opening a compressed part for reading from a `ReadWrite` package always decompressed the entire entry into a `MemoryStream` before it returned the stream. This approach produced a seekable stream, but it imposed unnecessary memory allocations and CPU overhead for the common case where callers only read the stream sequentially (for example, `XmlReader` or the Open XML SDK). The new behavior streams directly from the underlying ZIP archive entry for forward-only reads, which avoids the up-front decompression. + +For more information, see [dotnet/runtime#129698](https://github.com/dotnet/runtime/pull/129698). + +## Recommended action + +If your code requires a seekable stream from a compressed part of a `ReadWrite` package, copy it into a `MemoryStream` manually: + +```csharp +using Stream partStream = part.GetStream(FileMode.Open, FileAccess.Read); +using MemoryStream seekable = new(); +partStream.CopyTo(seekable); +seekable.Position = 0; +// Use 'seekable'. It's fully buffered and seekable. +``` + +Alternatively, if your use case doesn't require `ReadWrite` access to the package, open it with `FileAccess.Read` instead. Read-only packages already returned forward-only streams before this change, so this mode avoids any surprise for seek-sensitive code. + +## Affected APIs + +- +- +- diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index aee280b97e29f..d1c6ee2d67e8c 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -24,9 +24,11 @@ items: href: core-libraries/11/namedpipeserverstream-unix-permissions.md - name: Nullable.GetUnderlyingType throws for custom Type subclasses href: core-libraries/11/nullable-getunderlyingtype-throws.md + - name: PackagePart.GetStream() returns non-seekable stream for compressed parts + href: core-libraries/11/packagepart-getstream-non-seekable.md - name: API obsoletions with non-default diagnostic IDs href: core-libraries/11/obsolete-apis.md - - name: SafeFileHandle.IsAsync and FileStream.IsAsync accurately reflect non-blocking state on Unix + - name: SafeFileHandle.IsAsync and FileStream.IsAsync accurately reflect non-blocking state href: core-libraries/11/safefilehandle-isasync-unix.md - name: TAR-reading APIs verify header checksums when reading href: core-libraries/11/tar-checksum-validation.md