Modify System.IO.Packaging files to ensure they are deterministic. Helpful for testing, build reproducibility, security verification, and ensuring package integrity across different build environments.
Example file formats that leverage System.IO.Packaging
- .nupkg
- Microsoft Office files
See Milestones for release notes.
- For an input package stream
- Duplicate each entry with Deflate compression and consistent order
- Omit
package/services/metadata/core-properties/*.psmdcpentries - Omit
.signature.p7sentries (NuGet package signatures are invalidated by the conversion since package contents are modified) - For all relationship entries (
.relsfiles)- Modify the
Idof eachRelationshipto be deterministic - Convert absolute
Targetpaths to relative (e.g.Target="/xl/workbook.xml"becomesTarget="xl/workbook.xml") - Order
Relationships byType
- Modify the
- For the relationships entry
_rels/.rels- Remove the
Relationshipfor the.psmdcpentry
- Remove the
- For the relationships entry
docProps/core.xml- Remove the
creator,created,lastModifiedBy, andmodifiedelements
- Remove the
The conversion throws if any spreadsheetml XML entry (e.g. xl/workbook.xml, xl/worksheets/sheet1.xml) uses a prefixed default namespace such as <x:worksheet xmlns:x="..."> instead of the unprefixed form <worksheet xmlns="...">. This is because tools like Microsoft Spreadsheet Compare cannot open files with prefixed spreadsheetml elements. The OpenXml SDK can produce this form — ensure source xlsx files use default namespace declarations.
Binary output may differ between .NET Framework (net48) and .NET (net10.0+) due to differences in Deflate compression implementations. The XML content within entries is identical — only the compressed bytes differ.
This applies to all package formats (xlsx, docx, nupkg, etc.). When snapshot-testing binary package output across multiple target frameworks using Verify, use UniqueForRuntime to generate framework-specific verified files:
await Verify(stream, extension: "xlsx")
.UniqueForRuntime();See Verify Naming docs for more details.
using var sourceStream = File.OpenRead(packagePath);
var target = DeterministicPackage.Convert(sourceStream);using var sourceStream = File.OpenRead(packagePath);
var target = await DeterministicPackage.ConvertAsync(sourceStream);Overloads report what the conversion changed: entries it removed, entries whose content it patched, and whether the package had to be reordered.
using var sourceStream = File.OpenRead(packagePath);
var target = DeterministicPackage.Convert(sourceStream, out var changes);
foreach (var change in changes)
{
Console.WriteLine($"{change.Kind} {change.Entry}");
}The async form returns the report beside the stream, since an async method cannot have an out parameter:
using var sourceStream = File.OpenRead(packagePath);
using var result = await DeterministicPackage.ConvertWithChangesAsync(sourceStream);
foreach (var change in result.Changes)
{
Console.WriteLine($"{change.Kind} {change.Entry}");
}Only differences are reported. The conversion restamps every timestamp, recompresses every entry and reserializes every XML part regardless, but those happen to every input alike and produce the same bytes on a second conversion — so reporting them would say nothing about why a given package was not already deterministic. An already deterministic package reports nothing at all.
Pi designed by Zaidan from The Noun Project.