Repository navigation
API
All types live in the FileHub namespace. Each driver ships a marker interface (e.g. ILocalFileHub) that inherits from IFileHub.
- IFileHub
- FileDirectory
- FileEntry
- FileSystemEntry (common base)
- IUrlAccessible
- IRefreshable
- FileHubException
public interface IFileHub
{
FileDirectory Root { get; }
}Entry point. Root is the sandbox; everything else is reached from it.
| Marker | Assembly |
|---|---|
ILocalFileHub |
FileHub |
IMemoryFileHub |
FileHub |
IOracleObjectStorageFileHub |
FileHub.OracleObjectStorage |
IFtpFileHub |
FileHub.Ftp |
Use a marker when a consumer must bind to a specific backend; use IFileHub when it's backend-agnostic.
public abstract class FileDirectory : FileSystemEntry
{
public abstract FileDirectory Parent { get; }
}| Sync | Async | Notes |
|---|---|---|
CreateFile(name) |
CreateFileAsync(name, ct) |
Create or truncate. |
CreateFile(name, overwrite) |
CreateFileAsync(name, overwrite, ct) |
overwrite: true ⇒ DeleteIfExists first. |
OpenFile(name) |
OpenFileAsync(name, ct) |
Throws if missing. |
OpenFile(name, createIfNotExists) |
OpenFileAsync(name, createIfNotExists, ct) |
Open-or-create. |
TryOpenFile(name, out FileEntry) |
— | Non-throwing. |
GetFiles(pattern, offset, limit) |
GetFilesAsync(pattern, offset, limit, ct) |
See Usage → Pagination. |
| Sync | Async | Notes |
|---|---|---|
CreateDirectory(name) |
CreateDirectoryAsync(name, ct) |
Accepts nested paths ("a/b/c"). |
OpenDirectory(name) |
OpenDirectoryAsync(name, ct) |
Throws if missing. |
OpenDirectory(name, createIfNotExists) |
OpenDirectoryAsync(name, createIfNotExists, ct) |
Open-or-create. |
TryOpenDirectory(name, out FileDirectory) |
— | Non-throwing. Accepts nested paths. |
GetDirectories(pattern) |
GetDirectoriesAsync(pattern, ct) |
| Sync | Async |
|---|---|
ItemExists(name) |
ItemExistsAsync(name, ct) |
Delete() / Delete(name) / DeleteIfExists(name)
|
DeleteAsync siblings |
Rename(newName) |
RenameAsync(newName, ct) |
MoveTo(dir, name) / CopyTo(dir, name)
|
MoveToAsync / CopyToAsync
|
var logs = hub.Root.CreateDirectory("2026/01/logs"); // nested ⇒ creates intermediates
logs.CreateFile("app.log").SetText("ready");
foreach (var f in hub.Root.GetFiles("*.log"))
Console.WriteLine(f.Path);Nested-path behaviour is controlled by DirectoryPathMode on the hub (Local/Memory default to OpenIntermediates; OCI defaults to Direct).
public abstract class FileEntry : FileSystemEntry
{
public string Extension { get; }
public abstract long Length { get; }
public abstract FileDirectory Parent { get; }
}| Sync | Async |
|---|---|
ReadAllText() / ReadAllText(Encoding)
|
ReadAllTextAsync(ct) / ReadAllTextAsync(Encoding, ct)
|
ReadAllBytes() |
ReadAllBytesAsync(ct) |
SetText(content, Encoding?) |
SetTextAsync(content, Encoding?, ct) |
SetBytes(buffer) |
SetBytesAsync(buffer, ct) |
GetReadStream() |
GetReadStreamAsync(ct) |
GetWriteStream() |
GetWriteStreamAsync(ct) |
CopyToStream(Stream) |
CopyToStreamAsync(Stream, ct) |
| Sync | Async |
|---|---|
Delete() |
DeleteAsync(ct) |
Rename(newName) |
RenameAsync(newName, ct) |
MoveTo(dir, name) |
MoveToAsync(dir, name, ct) |
CopyTo(newName) / CopyTo(dir, name)
|
CopyToAsync(...) — cross-driver safe (falls back to stream copy) |
var file = hub.Root.OpenFile("report.pdf");
await file.CopyToAsync(otherHub.Root, "report.pdf", ct); // works across any two driversShared base for FileEntry and FileDirectory.
public abstract class FileSystemEntry : IDisposable
{
public abstract string Path { get; }
public string Name { get; protected set; }
public bool IsReadOnly { get; protected set; }
public abstract DateTime CreationTimeUtc { get; }
public abstract DateTime LastWriteTimeUtc { get; }
public abstract bool Exists();
public virtual Task<bool> ExistsAsync(CancellationToken ct = default);
protected void ThrowIfReadOnly(); // used by driver writes
protected static void ValidateName(string); // blocks empty, '.', '..', invalid chars
}-
CreationTimeUtc/LastWriteTimeUtcare cached values. Local/Memory read them straight from the in-process / OS state on access; cloud drivers (OCI, FTP) return whatever was last fetched and requireRefresh()for a round-trip. Writes through cloud drivers update the cached length locally sofile.LengthafterSetTextis correct without a refresh. -
Exists()may returnfalseafterDelete(); the object itself stays alive. -
IsReadOnly = truecauses every write to throwFileHubException— see Security → Read-only.
Optional interface a FileEntry may implement when the file is reachable through a URL.
public interface IUrlAccessible
{
bool IsPublic { get; }
Uri GetPublicUrl();
Uri GetSignedUrl(TimeSpan expiresIn);
Task<Uri> GetSignedUrlAsync(TimeSpan expiresIn, CancellationToken ct = default);
}| Driver | Implements? |
|---|---|
LocalFile, MemoryFile
|
No |
OracleObjectStorageFile |
Yes |
if (file is IUrlAccessible url)
{
var link = url.IsPublic
? url.GetPublicUrl()
: await url.GetSignedUrlAsync(TimeSpan.FromMinutes(15), ct);
return Redirect(link.ToString());
}
return PhysicalFile(file.Path, "application/octet-stream");GetPublicUrl() throws InvalidOperationException if IsPublic == false. GetSignedUrl throws ArgumentOutOfRangeException when expiresIn <= TimeSpan.Zero.
Optional interface implemented by FileEntry / FileDirectory whose metadata is cached and must be re-fetched explicitly. Exists so property getters can stay cheap and non-blocking — hidden async-over-sync inside a getter risks deadlock under UI / ASP.NET (classic) SynchronizationContexts.
public interface IRefreshable
{
void Refresh();
Task RefreshAsync(CancellationToken ct = default);
}| Driver | File / Directory implement it? |
|---|---|
LocalFile, LocalDirectory
|
No (OS tracks it — no cache) |
MemoryFile, MemoryDirectory
|
No (state is authoritative) |
OracleObjectStorageFile, OracleObjectStorageDirectory
|
Yes |
FtpFile, FtpDirectory
|
Yes |
var file = hub.Root.OpenFile("data.csv"); // stat'd at open, metadata populated
// ... time passes, bucket mutated externally ...
if (file is IRefreshable r) await r.RefreshAsync(ct);
Console.WriteLine(file.Length); // server-authoritative nowWrites through the driver update the cached length as bytes stream, so the common SetText → file.Length flow works without an explicit refresh. See Usage → Refreshing metadata.
public class FileHubException : IOException
{
public FileHubException(string message);
public FileHubException(string message, Exception innerException);
}Extends IOException so callers that already catch I/O errors match it automatically.
| Thrown when… | Exception type |
|---|---|
Sandbox escape (.., absolute path, symlink out of root) |
FileHubException |
| Write on a read-only entry | FileHubException |
| Driver wraps an I/O-shaped backend failure | FileHubException |
| File missing and not creating | FileNotFoundException |
| Directory missing | DirectoryNotFoundException |
null / empty / invalid name (.., ., path chars) |
ArgumentException |
| Stream re-open on OCI while another is live | InvalidOperationException |
try
{
hub.Root.OpenDirectory("../escape");
}
catch (FileHubException ex)
{
logger.LogWarning(ex, "Blocked traversal attempt");
}Driver authors writing their own backend: wrap SDK-level failures that look like an I/O problem in FileHubException — see Custom Drivers.
FileHub — unified file & directory API for .NET. Core is dependency-free; drivers are opt-in packages.