Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions docs/core/compatibility/11.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <xref:System.IO.Packaging.Package> with <System.IO.FileAccess.ReadWrite?displayProperty=nameWithType> and then open a compressed part for reading that hasn't been modified in the current session, <xref:System.IO.Packaging.PackagePart.GetStream*?displayProperty=nameWithType> now returns a forward-only stream instead of a seekable <xref:System.IO.MemoryStream>.

## 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

- <xref:System.IO.Packaging.PackagePart.GetStream?displayProperty=fullName>
- <xref:System.IO.Packaging.PackagePart.GetStream(System.IO.FileMode)?displayProperty=fullName>
- <xref:System.IO.Packaging.PackagePart.GetStream(System.IO.FileMode,System.IO.FileAccess)?displayProperty=fullName>
4 changes: 3 additions & 1 deletion docs/core/compatibility/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
gewarren marked this conversation as resolved.
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
Expand Down
Loading