-
-
Notifications
You must be signed in to change notification settings - Fork 38
Migration from DiscUtils to LTRData.DiscUtils
Audience: maintainers of applications and libraries built against the original DiscUtils packages.
API comparison: 2 September 2026. Package and registration guidance updated: 12 September 2026.
This page focuses on source and behavior changes when migrating existing code. For the current package catalog, format capabilities, target frameworks and platform requirements, see the repository README and the package-specific READMEs linked from it.
The detailed API comparison uses these historical baselines:
- original DiscUtils 0.15.0, commit
590a9281cceaf6d24eda721b29bda05848624156; and - LTRData.DiscUtils 1.0.88, comparison commit
6da57ec995a45f170508c826abfb00480dd25b9a.
0.15.0 is the appropriate historical baseline because its tag exists in both repositories and points to the same commit. It is therefore the last stable, unambiguous shared revision—not merely a nearby upstream release. The comparison used compiled netstandard2.0 assemblies and then checked important findings against source.
The commit-pinned source links below preserve that comparison; they are not links to the latest implementation. In the API examples, “Old” and “Current” distinguish these two baselines. Package and registration guidance follows the current repository and may require a newer package build than the comparison baseline. Compile and validate against the exact package version and target frameworks you use; see project-specific validation.
For an ordinary application, migration is usually mechanical:
- Replace
DiscUtils.*package references withLTRData.DiscUtils.*packages, while retaining existingDiscUtils.*namespaces. - Register the required providers before using automatic format detection or generic disk APIs.
- Change assumptions about directory/detection/build results from arrays to
IEnumerable<T>or read-only collections. - Replace removed helper names such as
ReadExactwithReadExactly. - Treat
Geometry/BiosGeometryas nullable and update Windows security type imports. - Decide deliberately whether to use the new async/Memory APIs; synchronous APIs remain broadly available.
Libraries that implement or subclass DiscUtils contracts need a separate porting pass. In particular, update enumeration return types, implement Span/Memory and async buffer members, add new interface properties/methods, and review new abstract/virtual disk contracts.
Replace the original NuGet package IDs with their LTRData.DiscUtils equivalents. Assembly names and C# namespaces remain largely DiscUtils.*; the NuGet prefix is not a namespace rename.
Old project:
<PackageReference Include="DiscUtils.Core" Version="0.15.0" />
<PackageReference Include="DiscUtils.Ntfs" Version="0.15.0" />Migrated project (using the comparison version):
<PackageReference Include="LTRData.DiscUtils.Core" Version="1.0.88" />
<PackageReference Include="LTRData.DiscUtils.Ntfs" Version="1.0.88" />Keep source imports such as using DiscUtils; and using DiscUtils.Ntfs;. Do not mechanically rewrite them to LTRData.DiscUtils.*.
For a broad existing dependency, start with LTRData.DiscUtils, get the port compiling, then narrow the referenced packages if appropriate. For applications that already use individual format packages, retain that selection.
| Original package | LTRData replacement |
|---|---|
DiscUtils |
LTRData.DiscUtils |
DiscUtils.Containers |
LTRData.DiscUtils.Containers |
DiscUtils.FileSystems |
LTRData.DiscUtils.FileSystems |
DiscUtils.Transports |
LTRData.DiscUtils.Transports |
DiscUtils.Core, DiscUtils.Ntfs, etc. |
LTRData.DiscUtils.Core, LTRData.DiscUtils.Ntfs, etc. |
The broad meta-package is named LTRData.DiscUtils; DiscUtils.Complete is the namespace of its setup helper, not the name of a separate LTRData.DiscUtils.Complete package. The README package overview links to each meta-package's exact dependencies and registration scope. Dokan/FUSE integrations are separate packages and are not included by the broad library meta-package.
Referencing or loading a format assembly does not register its providers. With the current registration API, register the providers needed by generic disk APIs and automatic detection during application startup:
DiscUtils.Core.Formats.Register(); // Includes RAW disks and the local-file transport
DiscUtils.Ntfs.Formats.Register();Each entry point registers only providers implemented in that assembly, not providers in its dependencies. Direct use of concrete format APIs does not require automatic discovery. ISO/UDF detection is supplied by DiscUtils.OpticalDisk.Formats.Register(); Iso9660 and Udf have no separate generated registration entry points.
If using a meta-package, choose its composition helper instead:
DiscUtils.Complete.SetupHelper.SetupComplete();
DiscUtils.Containers.SetupHelper.SetupContainers();
DiscUtils.FileSystems.SetupHelper.SetupFileSystems();
DiscUtils.Transports.SetupHelper.SetupTransports();These are alternatives for different package selections, not a sequence of required calls. Package dependencies, available discovery providers and setup-helper registration scope are distinct; use the package README to check the helper's coverage.
Explicit registration is repeatable and compatible with trimming and Native AOT. This describes registration, not every operation in every dependency. Applications do not need to run a generator or rely on module initializers.
For reflection-based plugin discovery on suitable JIT runtimes, the entry point is:
DiscUtils.Setup.SetupHelper.RegisterAssembly(assembly);Use explicit registration for trimming/Native AOT. See format registration and Native AOT for third-party providers, older reflection-based setup and registration limits.
Original 0.15.0 libraries targeted netstandard2.0, netstandard1.5, net20, net40, and net45 (old common properties). The LTRData comparison baseline targets netstandard2.0, netstandard2.1, net46, net48, net8.0, net9.0, and net10.0 (baseline properties).
Consequences:
- .NET 2.0, 4.0, and 4.5 applications must retarget (at least .NET Framework 4.6) or cannot consume current packages.
-
netstandard1.xconsumers should move tonetstandard2.0or a supported modern .NET target. - A low-risk migration is normally two-step: first move the existing application or library to the LTRData.DiscUtils .NET Standard 2.0 assets, then upgrade the consumer's own TFM and let NuGet select the matching newer LTRData.DiscUtils assets. The public contracts discussed in this guide can reasonably be expected to work the same way on newer TFMs.
- Multi-targeted libraries should still compile every TFM they publish, particularly where framework-provided stream helpers can affect overload binding.
- Old 0.15.0 builds were signed; LTRData.DiscUtils intentionally dropped strong naming after it needed to import an unsigned dependency. Binary binding, strong-name-qualified reflection,
InternalsVisibleTo, and GAC scenarios therefore require migration work. Ordinary NuGet source consumers normally only rebuild. This is the current policy, although maintainers may reconsider it if the dependency tree no longer requires unsigned assemblies.
This is one of the most widespread consumer changes in the API comparison. It affects IFileSystem/DiscFileSystem, DiscDirectoryInfo, file-system implementations, DNS/iSCSI helpers, parent-location enumeration, cluster/path mapping, builders, and detection factories.
IEnumerable<T> describes the result contract, not a guaranteed evaluation strategy. A GetFiles implementation may return immediately and discover each file lazily, buffer some directory state before yielding, or return an already materialized array through the interface. Consumers must not rely on when I/O occurs, how much is buffered, or whether repeated enumeration repeats the underlying work unless the concrete implementation documents it.
Old:
string[] files = fs.GetFiles(@"\data", "*.bin");
if (files.Length != 0)
Process(files[0]);
DiscFileInfo[] infos = fs.Root.GetFiles();Current:
IEnumerable<string> files = fs.GetFiles(@"\data", "*.bin");
foreach (string file in files)
Process(file);
// Materialize only when indexing, repeated enumeration, or a snapshot is required.
string[] snapshot = files.ToArray();
IReadOnlyList<DiscFileInfo> infos = fs.Root.GetFiles().ToArray();The relevant contracts are the old IFileSystem and current IFileSystem, plus current DiscDirectoryInfo.
Migration cautions:
-
.Lengthbecomes.Count()only if enumerating is acceptable; otherwise use.ToArray()once. - Indexing requires materialization or
First/ElementAt. - Exceptions and I/O may occur at the method call, during enumeration, or both.
- Keep the file system/stream alive until enumeration completes, even if a particular implementation currently materializes eagerly.
- Do not enumerate twice unless the API documents a stable, repeatable result; either pass the sequence through once or materialize an explicit snapshot.
Related concrete changes include:
-
FileSystemManager.DetectFileSystems:FileSystemInfo[]→ReadOnlyCollection<FileSystemInfo>. -
VfsFileSystemFactory.Detect: arrays →IEnumerable<FileSystemInfo>. -
DiskImageBuilder.Build:DiskImageFileSpecification[]→IEnumerable<DiskImageFileSpecification>. -
VirtualDiskLayer.GetParentLocationsand many format implementations: arrays →IEnumerable<string>. -
IClusterBasedFileSystem.PathToClusters: array →IEnumerable<Range<long,long>>.
Current IFileSystem adds the three-argument GetFileSystemEntries(path, searchPattern, SearchOption) contract alongside lazy enumeration. Code that only calls existing overloads usually needs no change. Code that reflects over exact overload sets, creates delegates to a particular return type, or mocks IFileSystem must update.
Old:
string[] entries = fs.GetFileSystemEntries(path, "*.txt");Current:
IEnumerable<string> entries =
fs.GetFileSystemEntries(path, "*.txt", SearchOption.AllDirectories);Disk geometry properties and parameters that formerly used a non-null Geometry value now commonly use Geometry?, including VirtualDisk.Geometry, VirtualDiskParameters.Geometry/BiosGeometry, DiskImageBuilder.Geometry/BiosGeometry, and many VHD/VHDX/VMDK initialization overloads. The old Geometry.Null sentinel is gone.
Old:
Geometry geometry = disk.Geometry;
if (geometry != Geometry.Null)
Console.WriteLine(geometry.Capacity);Current:
Geometry? geometry = disk.Geometry;
if (geometry is { } value)
Console.WriteLine(value.Capacity);For creation calls, pass null when geometry should be inferred. If overload resolution becomes ambiguous after replacing Geometry.Null, cast explicitly to (Geometry?)null.
Also review integer width/sign changes: DiscFileSystem.VolumeId and FAT VolumeId are now uint rather than int. Avoid unchecked casts for volume IDs with the high bit set.
NTFS/WIM and IWindowsFileSystem no longer expose System.Security.AccessControl.RawSecurityDescriptor and System.Security.Principal.SecurityIdentifier. They expose equivalents under:
DiscUtils.Core.WindowsSecurity.AccessControl.RawSecurityDescriptor
DiscUtils.Core.WindowsSecurity.SecurityIdentifierOld:
using System.Security.AccessControl;
RawSecurityDescriptor sd = ntfs.GetSecurity(path);Current:
using DiscUtils.Core.WindowsSecurity.AccessControl;
RawSecurityDescriptor sd = ntfs.GetSecurity(path);This is a source and type-identity break, not just a namespace alias. Update helper methods, DTOs, generic constraints, and serializers that mention the BCL types. It matters most to Windows consumers that also pass security descriptors to or from Windows APIs. Prefer conversion through SDDL or binary form at that boundary; do not cast. The new contract is visible in current IWindowsFileSystem versus the old contract.
DiscUtils.Streams.StreamUtilities.ReadExact was renamed to ReadExactly, matching modern .NET terminology, and gained Span/Memory and async overloads. EndianUtilities and writer APIs similarly favor spans.
Old:
byte[] header = StreamUtilities.ReadExact(stream, 512);
StreamUtilities.ReadExact(stream, buffer, 0, buffer.Length);Current:
byte[] header = stream.ReadExactly(512);
stream.ReadExactly(buffer.AsSpan());
await stream.ReadExactlyAsync(buffer.AsMemory(), cancellationToken);See old helpers and current helpers. On modern .NET, System.IO.Stream.ReadExactly also exists; extension-method calls may bind to the framework method. Prefer instance syntax where behavior matches, or qualify DiscUtils.Streams.StreamUtilities if exact binding matters.
Other common mechanical substitutions:
ReadExact(...) -> ReadExactly(...)
byte[] + offset/count -> Span<byte>/ReadOnlySpan<byte>
byte[] across await -> Memory<byte>/ReadOnlyMemory<byte>
Geometry.Null -> null
array.Length / array[index] -> materialize, Count(), First(), or foreach
BlockCompressor -> IBlockCompressor (+ IBlockDecompressor if needed)
BlockCompressor.Compress/Decompress was replaced by Span-based IBlockCompressor.TryCompress and IBlockDecompressor.TryDecompress; NtfsOptions.Compressor now uses the interface. Custom compressors need an implementer migration, described below.
LTRData adds async methods throughout buffers, streams, filesystems, iSCSI, builders, and disk formats, generally returning ValueTask, ValueTask<T>, or Task<T> and taking CancellationToken. Examples include ReadExactlyAsync, IBuffer.ReadAsync/WriteAsync/ClearAsync, DiscFileSystem.ReadAllBytesAsync, and VHDX initialization methods.
Existing synchronous code can generally remain synchronous. Do not replace every call merely because an async alternative exists. Convert end-to-end I/O paths, await ValueTask directly, and pass a real cancellation token.
Some disk constructors/open methods now accept bool useAsync (for example VHD/VHDX/VMDK path constructors and VirtualDisk.OpenDisk). This selects async-friendly underlying file access; it does not make the constructor itself awaitable.
Old:
using VirtualDisk disk = VirtualDisk.OpenDisk(path, FileAccess.Read);Current synchronous:
using VirtualDisk disk = VirtualDisk.OpenDisk(path, FileAccess.Read);Current async-oriented I/O path:
using VirtualDisk disk = VirtualDisk.OpenDisk(path, FileAccess.Read, useAsync: true);
int read = await disk.Content.ReadAsync(buffer, cancellationToken);The compiled netstandard surface no longer contains System.HashCode and System.DateTimeOffsetExtensions polyfills. Code should use framework System.HashCode and DateTimeOffset.FromUnixTimeSeconds/ToUnixTimeSeconds on supported targets.
DiscUtils.Streams.BuilderBytesExtent is removed as part of a redesign of the entire extent-based builder architecture. There is no direct replacement. Code that constructed it or derived behavior from the old extent lifecycle needs a deeper redesign against the current builder model; substituting a similarly named extent class is not a safe mechanical migration. Where possible, move to a higher-level format builder rather than recreating the old architecture.
The NTFS-internals enums DiscUtils.Ntfs.Internals.AttributeFlags and NtfsFileAttributes are no longer present and have no direct replacements. Code using them must be redesigned around supported current APIs rather than mapping enum values blindly.
Avoid relying on undocumented implementation details merely because their types are public. However, namespace names alone do not determine support: VirtualDiskTransport, LogicalVolumeFactory and their discovery attributes are documented public extension points in the current registration API, despite retaining the DiscUtils.Internal namespace. Consult the third-party format documentation before replacing or removing custom provider code.
Every enumeration method must return IEnumerable<string>, and GetFileSystemEntries(path, pattern, searchOption) is part of the contract. SupportsUsedAvailableSpace is newly required. DiscFileSystem also exposes new RawStream and VolumeId properties; derived classes should inspect abstractness for each target and base class revision rather than hiding them.
Old implementation:
public override string[] GetFiles(
string path, string pattern, SearchOption option)
=> BuildFileList(path, pattern, option).ToArray();Current implementation:
public override IEnumerable<string> GetFiles(
string path, string pattern, SearchOption option)
{
foreach (var item in EnumerateFiles(path, pattern, option))
yield return item;
}
public override bool SupportsUsedAvailableSpace => true;If size accounting is unavailable, return false for SupportsUsedAvailableSpace and ensure the space properties fail or report values consistently with LTRData implementations. Do not return an empty sequence merely to defer an unimplemented enumeration path.
Because C# cannot overload solely on return type, array-returning overrides must be changed in place. A compatibility helper can expose a differently named GetFilesArray method for downstream old callers.
The old IBuffer centered on byte[] buffer, int offset, int count. Current IBuffer adds/requires:
int Read(long pos, Span<byte> buffer);
void Write(long pos, ReadOnlySpan<byte> buffer);
ValueTask<int> ReadAsync(long pos, Memory<byte> buffer, CancellationToken token);
ValueTask WriteAsync(long pos, ReadOnlyMemory<byte> buffer, CancellationToken token);
ValueTask ClearAsync(long pos, int count, CancellationToken token);Compare old IBuffer and current IBuffer.
Minimal port pattern:
public int Read(long pos, Span<byte> destination)
{
// Prefer a native Span implementation; avoid destination.ToArray().
return ReadCore(pos, destination);
}
public ValueTask<int> ReadAsync(
long pos, Memory<byte> destination, CancellationToken token)
=> ReadCoreAsync(pos, destination, token);Do not implement async members as new ValueTask<int>(Read(...)) if the underlying storage can block. That compiles but defeats the fork’s async objective. A synchronous wrapper is acceptable only for genuinely memory-backed buffers and should honor cancellation before work.
Derived classes of DiscUtils.Streams.Buffer now override Span-based Read/Write. Old array overrides will not satisfy the abstract members.
Old:
int ReadFrom(byte[] buffer, int offset);
void WriteTo(byte[] buffer, int offset);Current:
int ReadFrom(ReadOnlySpan<byte> buffer);
void WriteTo(Span<byte> buffer);Port offset arithmetic by slicing at the call site:
// old
record.ReadFrom(bytes, offset);
// current
record.ReadFrom(bytes.AsSpan(offset));Inside implementations, index relative to the received span. Do not preserve the old offset and also slice, or fields will be displaced twice. Similar changes apply to BuilderExtent.Read, HFS+ B-tree records, SCSI commands, and many format-specific record bases.
IClusterBasedFileSystem now requires/contains:
-
IEnumerable<Range<long,long>> PathToClusters(string path); -
long GetAllocatedClustersCount(string path); -
int SectorSize; -
long TotalSectors; - allocation-extents support through
IAllocationExtentsFileSystem.
The contract is in current source, compared with old source.
IWindowsFileSystem is now composed from IDosFileSystem and IFileSystemWithAltStreams, uses DiscUtils-owned security types, and adds GetHardLinkCount(string path). Implementations should return the actual link count, not merely 1, when the format exposes it.
IVfsFile adds IEnumerable<StreamExtent> EnumerateAllocationExtents(). IVfsDirectory.AllEntries changes from ICollection<TDirEntry> to IReadOnlyDictionary<string,TDirEntry>, so implementations must preserve name-to-entry lookup and define the comparer/case behavior consistently with the filesystem. See current IVfsFile.
Important abstract contract changes include:
-
VirtualDisk.CreateDifferencingDisk(string)is no longer the abstract extension point. Current subclasses implementCreateDifferencingDisk(string, bool useAsync)andCreateDifferencingDisk(DiscFileSystem, string); the one-argument method is a convenience wrapper. -
VirtualDisk.GeometryisGeometry?;CanWriteis part of the current surface. -
VirtualDiskLayer.GetParentLocations()returnsIEnumerable<string>and the layer exposesRelativeFileLocator,CanWrite, andCapacity. -
DiskImageBuilder.Build(string)returnsIEnumerable<DiskImageFileSpecification>and its geometry properties are nullable. -
VfsFileSystemFactory.Detect(Stream, VolumeInfo)returnsIEnumerable<FileSystemInfo>. -
VirtualDiskFactoryis public in the current surface and has revised creation/opening methods, including async-mode opening. Treat it as a current extension point only after reviewing its registration expectations.
Old disk subclass:
public override VirtualDisk CreateDifferencingDisk(string path) { ... }
public override Geometry Geometry => _geometry;Current disk subclass:
public override VirtualDisk CreateDifferencingDisk(string path, bool useAsync) { ... }
public override VirtualDisk CreateDifferencingDisk(
DiscFileSystem fileSystem, string path) { ... }
public override Geometry? Geometry => _geometry;
public override bool CanWrite => _content.CanWrite;Review old VirtualDisk, current VirtualDisk, and both versions of DiskImageBuilder.
DiscUtils.Compression.BlockCompressor is replaced by interfaces:
public interface IBlockCompressor
{
CompressionResult TryCompress(
ReadOnlySpan<byte> source,
Span<byte> compressed,
out int compressedLength);
}
public interface IBlockDecompressor
{
int BlockSize { get; set; }
bool TryDecompress(
ReadOnlySpan<byte> source,
Span<byte> decompressed,
out int decompressedSize);
}Custom NTFS compressors should normally implement both interfaces, preserve BlockSize, validate destination capacity, and return the expected failure/result status rather than throwing for ordinary “output did not fit” cases. Update NtfsOptions.Compressor assignments to IBlockCompressor.
The old builder extent hierarchy exposed PrepareForRead, DisposeReadState, and/or Dispose; the current Span-oriented builder system has a different architecture and lifecycle, and BuilderBytesExtent is absent. This is not an override-signature-only port. Derived extent classes must first be redesigned around the current builder/source ownership model; only then should their reading logic be expressed through current members such as Read(long, Span<byte>) where applicable.
Do not choose a current extent type merely because its surface looks similar, and do not retain resource ownership assumptions from the old IDisposable-like pattern. Review the current builder that consumes the extent end to end. This is a high-risk area for behavioral errors, file-handle leaks, and premature disposal.
For consumers:
- Change package IDs to
LTRData.DiscUtils.*; keepDiscUtils.*namespaces. - Retarget unsupported frameworks.
- Add setup/registration before detection.
- Compile, then fix
T[]assumptions at use sites; materialize only where necessary. - Replace
Geometry.Nulland non-null geometry locals with nullable handling. - Import DiscUtils Windows-security namespaces and convert at OS boundaries.
- Replace
ReadExactwithReadExactly; adopt Span/Memory overloads where useful. - Check signed-assembly assumptions and reflection strings.
- Run behavior tests that enumerate after method return and after stream disposal.
For implementers/subclasses:
- Recompile every supported TFM to expose conditional contract gaps.
- Update all array-returning interface/override methods to
IEnumerable<T>. - Implement
SupportsUsedAvailableSpaceand the three-argument file-system entry enumeration. - Implement native Span members, then real async Memory members for
IBuffer. - Convert
IByteArraySerializableand binary record overrides to spans. - Add cluster count/sector properties, hard-link count, and VFS allocation extents.
- Rework
IVfsDirectory.AllEntriesas a read-only dictionary. - Update disk differencing, nullable geometry, layer, factory, builder, and compressor contracts.
- Audit resource ownership after builder/extent changes.
- Add compile-only compatibility tests for representative downstream code as well as runtime image/filesystem tests.
For a library with many downstream users, a short-lived adapter layer is safer than a flag day:
public static class DiscUtilsLegacyExtensions
{
public static string[] GetFilesArray(
this IFileSystem fs, string path, string pattern)
=> fs.GetFiles(path, pattern).ToArray();
public static byte[] ReadExactLegacy(this Stream stream, int count)
=> stream.ReadExactly(count);
}Do not try to reproduce old members with the same name and only a different return type; C# cannot resolve that. Keep shim names explicit, mark them obsolete, and remove them in a scheduled major release.
For security types, use explicit conversion helpers. For buffers and serializers, adapt at the edge with .AsSpan(...)/.AsMemory(...); avoid allocating new arrays on every call, or the principal performance benefit of the fork is lost.
In addition to compiling against the chosen package version and reviewing the signing changes above, validate these behaviors in your application:
-
Enumeration evaluation strategy.
IEnumerable<T>permits fully lazy, partly buffered, and already-materialized implementations. Do not depend on exact evaluation timing, buffering, or repeatability without implementation-specific tests. - Windows security interoperability. DiscUtils-owned ACL/SID types mirror Windows concepts, but type identity differs. This principally affects consumers that cross-reference descriptors with Windows APIs while running on Windows. Existing project tests provide limited validation of these boundary conversions, so validate SDDL and binary round trips against the ACL corpus used by your application.
For the original API comparison, the public netstandard2.0 assemblies at the two pinned revisions were built and reflected. The comparison contained 3,447 baseline and 5,568 current public type/member records; it found only a handful of removed named types but hundreds of changed signatures and over two thousand additions. Findings were triaged for realistic source breaks, then checked against repository source and build metadata. Generated/compiler-detail noise and purely additive format features were intentionally excluded.