Interop and Migration
Overview
This article is a practical guide for code that already uses System.IO, either because you're migrating an existing application or because you depend on a third-party library that takes string, FileInfo or DirectoryInfo parameters. The library is designed to coexist with System.IO so you can adopt it incrementally.
Bridging at the Boundaries
The two key bridges are:
PathExport: convert a path to a string the underlying file system will reliably accept.SystemExtensions.ToPath: convertFileInfo/DirectoryInfoto a path object.
Use them at the boundaries of your code where it interacts with non-library APIs. Inside your own code, work with path objects.
Strings → Paths
To turn a string you got from somewhere else into a path, parse it with the parser that matches what you know about the input:
IAbsoluteFilePath path = FilePath.ParseAbsolute(stringFromExternalApi, PathOptions.None);
Use PathOptions.None when the string came from the file system itself (e.g. an OS file picker). The OS may surface "unfriendly" paths that exist and need to be opened. Use PathOptions.NoUnfriendlyNames (the default) for application-defined or user-typed input.
See Parsing Paths and PathOptions.
Paths → Strings
When an external API takes a string path, hand it PathExport (only available on absolute paths):
ThirdPartyApi.OpenFile(file.PathExport);
Important
Use PathExport, not PathDisplay or ToString(), when calling APIs outside this library. PathExport is specially formatted (e.g. with \\?\ on Windows) so the OS won't silently rewrite it.
FileInfo / DirectoryInfo → Paths
Use the SystemExtensions extension methods:
using Singulink.IO;
DirectoryInfo di = new(@"C:\some\path");
FileInfo fi = new(@"C:\some\file.txt");
IAbsoluteDirectoryPath dirPath = di.ToPath();
IAbsoluteFilePath filePath = fi.ToPath();
Both extensions parse FullName with PathOptions.NoUnfriendlyNames by default; pass PathOptions.None to accept any path:
IAbsoluteFilePath filePath = fi.ToPath(PathOptions.None);
Caution
FileSystemInfo.FullName may have already been rewritten by System.IO (trimming trailing spaces and dots) before ToPath sees it. If preserving the exact original characters matters, parse the original string directly with FilePath.ParseAbsolute.
Paths → FileInfo / DirectoryInfo
When an external API takes a FileInfo or DirectoryInfo, construct one from PathExport:
FileInfo fi = new(absoluteFilePath.PathExport);
DirectoryInfo di = new(absoluteDirectoryPath.PathExport);
ThirdPartyApi.Process(fi);
System.IO ↔ Library Mapping
A reference of common System.IO operations and their library equivalents:
Path Construction
System.IO |
Library |
|---|---|
Path.Combine(a, b) |
dir.CombineFile(b) / dir.CombineDirectory(b) / dir + relative |
Path.GetDirectoryName(p) |
path.ParentDirectory.PathDisplay |
Path.GetFileName(p) |
path.Name |
Path.GetFileNameWithoutExtension(p) |
filePath.NameWithoutExtension |
Path.GetExtension(p) |
filePath.Extension |
Path.ChangeExtension(p, e) |
filePath.WithExtension(e) |
Path.GetFullPath(p) |
FilePath.ParseAbsolute(p) (or Parse if it could be relative) |
Path.GetTempPath() |
DirectoryPath.GetTemp() |
Path.GetTempFileName() |
FilePath.CreateTempFile() |
Files
System.IO |
Library |
|---|---|
File.Exists(p) |
filePath.Exists |
File.Open(p, mode) |
filePath.OpenStream(mode) |
File.Copy(s, d, overwrite) |
s.CopyTo(d, overwrite) |
File.Move(s, d, overwrite) |
s.MoveTo(d, overwrite) |
File.Replace(s, d, b) |
s.Replace(d, b) |
File.Delete(p) |
filePath.Delete() |
File.GetAttributes(p) |
filePath.Attributes |
File.GetLastWriteTimeUtc(p) |
filePath.LastWriteTimeUtc |
new FileInfo(p) (for metadata) |
filePath.GetInfo() (returns CachedFileInfo) |
Directories
System.IO |
Library |
|---|---|
Directory.Exists(p) |
dirPath.Exists |
Directory.CreateDirectory(p) |
dirPath.Create() |
Directory.Delete(p, recursive) |
dirPath.Delete(recursive) |
Directory.GetCurrentDirectory() |
DirectoryPath.GetCurrent() |
Directory.SetCurrentDirectory(p) |
DirectoryPath.SetCurrent(dirPath) |
Directory.GetParent(p) |
path.ParentDirectory |
Directory.GetFiles(d, pattern, opt) |
dirPath.GetChildFiles(pattern, options) |
Directory.GetDirectories(d, pattern, opt) |
dirPath.GetChildDirectories(pattern, options) |
Directory.EnumerateFileSystemEntries(d) |
dirPath.GetChildEntries() |
Drives
System.IO |
Library |
|---|---|
DriveInfo.GetDrives() |
DirectoryPath.GetMountingPoints() |
DriveInfo.AvailableFreeSpace |
dirPath.AvailableFreeSpace (on any absolute directory) |
DriveInfo.TotalFreeSpace |
dirPath.TotalFreeSpace |
DriveInfo.TotalSize |
dirPath.TotalSize |
DriveInfo.DriveType |
dirPath.DriveType |
DriveInfo.DriveFormat |
dirPath.FileSystem |
See Drive and Disk Information.
Migration Strategy
A pragmatic order of operations:
- Adopt at the edges first. New code parses inputs into path objects; old code keeps working with strings until you touch it.
- Use
ToPathto bridge existingFileInfo/DirectoryInfochains without rewriting the calling code. - Replace
try/catchladders with the parse-vs-IO split. Anything inside a parse step catchesArgumentException; anything inside an I/O step catchesIOException. See Exception Handling. - Convert
Directory.GetFiles/EnumerateFilescalls toGetChild*enumeration withSearchOptions. Pay attention toMatchCasing: the library's case-insensitive default is consistent across platforms, which is a behavior change fromSystem.IOon Unix. - Drop
DriveInfoworkarounds. UNC paths, mounted subdirs and per-user quotas Just Work via the disk-space members on absolute directories.
When You Still Need Strings
You'll still encounter APIs that demand a string. Continue using PathExport:
public Task UploadAsync(IAbsoluteFilePath file)
{
return _httpClient.PostAsync(_url, new StreamContent(File.OpenRead(file.PathExport)));
}
Don't use PathDisplay for I/O even if it looks the same as PathExport for typical paths; the difference is invisible until something subtle goes wrong.
Next Steps
- Path Formats: the difference between
PathDisplay,PathExportandToString(). - Exception Handling: replace
System.IOcatch ladders with the cleaner two-phase model.