Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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 seeks the stream or reads `Position`.
Comment thread
gewarren marked this conversation as resolved.
Outdated
Comment thread
gewarren marked this conversation as resolved.
Outdated

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