Repository navigation
FAQ
Quick answers to the questions newcomers hit most. Each entry links to the deeper page when there is one.
| Use case | Driver |
|---|---|
| Production on a single VM / on-prem | LocalFileHub |
| Unit tests, in-process state | MemoryFileHub |
| AWS S3 general-purpose buckets | AmazonS3FileHub |
| Oracle Cloud Object Storage | OracleObjectStorageFileHub |
| Plain FTP server | FtpFileHub |
| Anything else (Azure Blob, GCS, SFTP, …) | Write a custom driver |
Not yet, planned. Track on the issue tracker. Until then, write a custom driver against the Azure SDK — the Custom drivers page walks through the contract; the existing S3 driver is the closest reference.
Default to IFileHub. Use the marker only when a consumer genuinely needs a backend-specific guarantee. Prefer capability interfaces such as IUrlAccessible, ISignedUploadable, or IMultipartUploadSignable where available rather than casting directly to the driver type. Stream-based multipart writes use the common FileWriteOptions.StreamPreference API.
Don't. Use MemoryFileHub instead — it's the real implementation, in-process, no shared state, zero setup. Mocking the interface re-implements the contract badly; the in-memory hub is the contract.
var hub = new MemoryFileHub();
var svc = new ReportService(hub);
svc.Save("report.pdf", "hello");
Assert.Equal("hello", hub.Root.OpenFile("report.pdf").ReadAllText());More patterns: Testing.
For files, directories, sandbox enforcement, and read-only wrapping — yes. Search patterns (GetFiles("*.log")) now use the same globbing rules across both drivers (*, ?, [abc] character classes), so a pattern that matches in tests matches in production.
Search (GetFiles, glob patterns) is deliberately case-insensitive on every backend, including LocalFileHub on Linux — this diverges from Directory.GetFiles, which is case-sensitive there. A pattern that matches in tests matches in production regardless of host OS.
The differences that remain:
- Disposal / handle exhaustion semantics — Memory has no OS handle limits.
- File system case sensitivity for direct name lookups (
OpenFile("A.txt")vsOpenFile("a.txt")) follows the host OS forLocalFileHub;MemoryFileHubis always case-insensitive. - Symlinks / reparse points — Memory has none.
When you need true disk behaviour in a test, point LocalFileHub at Path.GetTempPath() and clean up afterwards.
Use MemoryFileHub for the service-under-test, since your code depends on IFileHub. The cloud driver's own internals are covered by FileHub's integration tests (tests/FileHub.AmazonS3.Tests/Integration/...) which only run when credentials are present.
Do not mock IAmazonS3 / ObjectStorageClient directly — those are SDK types; the driver wraps them behind an internal contract you can't reach. If you need to assert "driver X was called", inject MemoryFileHub and assert on its state instead.
| Condition | Type |
|---|---|
| Missing file | FileNotFoundException |
| Missing directory | DirectoryNotFoundException |
Sandbox escape (.., absolute, symlink out of root) |
FileHubException |
| Write on a read-only entry | FileHubException |
Invalid name (null, empty, ., .., path chars, or a //\ passed to Rename) |
ArgumentException |
| Access denied by the backend (all drivers incl. Local) | UnauthorizedAccessException |
| Driver wraps an I/O-shaped backend failure |
FileHubException (inner = SDK exception) |
| Stream re-open on S3 / OCI while another is live | InvalidOperationException |
Copy succeeded, source delete failed during MoveTo
|
PartialMoveException |
Deleting a non-empty directory without recursive: true
|
DirectoryNotEmptyException |
Copy/move onto an existing entry (default overwrite: false), CreateFile(name) onto an existing target, or Rename onto a taken name |
FileAlreadyExistsException |
FileHubException : IOException and FileAlreadyExistsException : FileHubException : IOException, so a generic IOException handler catches every FileHub error — the refined types just let you catch narrower where you want to.
UnauthorizedAccessException is surfaced as-is on every driver (including Local) — it is no longer wrapped in FileHubException.
Most common cause: pointing a AmazonS3FileHub at an S3 Express One Zone (Directory) bucket. Those use a different auth flow (CreateSession) and are not supported. Verify the bucket type in the AWS console; if --x-s3 is at the end of the name, it's a Directory bucket.
For OCI: check that the user / instance principal has both OBJECT_READ and OBJECT_INSPECT on the bucket. The GetNamespace and HeadBucket calls at construction time need the latter.
Object-storage and FTP drivers only allow one stream open per file at a time (read or write). Either dispose the first stream before opening the second, or use a different FileEntry instance.
using (var r = file.GetReadStream()) { /* read */ }
using (var w = file.GetWriteStream()) { /* write */ } // OK — first one disposedObject storage has no native "delete prefix" primitive — the driver lists every object and issues one DeleteObject per key. Cost grows linearly with object count. For large prefixes, prefer an S3 Lifecycle rule / OCI Lifecycle policy instead. Details: S3 → Recursive delete cost, OCI → Recursive delete cost.
There is no metadata-only update API. Write the object again with FileWriteOptions (or the S3 subclass S3WriteOptions) — this re-uploads the bytes with the new metadata applied. Note that LocalFileHub and FtpFileHub silently ignore metadata fields (their backends have no per-object metadata API).
var file = hub.Root.OpenFile("doc.pdf");
var bytes = file.ReadAllBytes();
file.SetBytes(bytes, new S3WriteOptions { StorageClass = "GLACIER" }); // 1 PutObjectFor storage-class transitions on large or many objects, prefer an S3 Lifecycle rule (background, no byte transfer through your process) over re-uploading — see S3 → Fields NOT exposed.
Index-based offsets force the driver to walk every object up to the offset. Cloud stores paginate by string cursor. Use FileListOffset.FromName(lastSeen) for deep paging:
hub.Root.GetFiles(offset: FileListOffset.FromName(lastSeen), limit: 100);Details: Usage → Pagination.
The cloud drivers route every sync overload through an internal SyncBridge that hops to the thread pool, defending against the common single-thread SynchronizationContext deadlock. It is not a universal guarantee — see Usage → Sync-over-async and deadlocks for the exact contract and edge cases. Default to async under any legacy UI / ASP.NET-classic host.
- Local / Memory — singleton.
- Cloud (S3, OCI) — singleton. The hub owns the SDK HTTP client; recreating per request wastes connections.
- FTP — singleton. The hub owns the control connection.
-
Per-tenant hubs — scoped, via
AddNamedFileHubs(..., ServiceLifetime.Scoped).
No. FileHub does not register hubs as keyed services. Inject INamedFileHubs and call GetByName("name") instead. The registry is scoped — don't capture it from a singleton.
Yes, via AddNamedFileHubs. Each entry gets its own name and lifetime. See Dependency Injection → Named hubs.
No. Every relative name passes through ValidateName (blocks null, empty, ., .., path chars) plus ResolveSafePath (blocks paths that fall outside the root, including symlink targets on .NET 8+). Pass user input directly into CreateFile(userName) — it's the supported tenant-isolation pattern.
var hub = new LocalFileHub($@"C:\tenants\{tenantId}");
hub.Root.CreateFile(userSuppliedName); // confined to that tenantDetails: Security.
Wrap with AsReadOnly(). Anything navigated from the wrapper is read-only too — there's no escape by walking the tree.
var ro = hub.Root.OpenDirectory("config").AsReadOnly();
RunPlugin(ro); // any CreateFile / Delete / Rename throws FileHubExceptionThe API is consolidating. Pre-v1 releases follow SemVer minor for breaking changes — pin to a minor ([0.x, 0.x+1)) until v1.0 ships. Drivers and the core ride the same version. File issues for anything surprising.
Open an issue on the issue tracker. Bug reports with a 10-line repro using MemoryFileHub get fixed fastest.
Drop a sketch in an issue first (which backend, which capabilities, what the auth surface looks like). Then implement against Custom drivers; the S3 / OCI / FTP drivers are reference implementations.
FileHub — unified file & directory API for .NET. Core is dependency-free; drivers are opt-in packages.