WriteableFileProviders.Physical
2027.1.1
dotnet add package WriteableFileProviders.Physical --version 2027.1.1
NuGet\Install-Package WriteableFileProviders.Physical -Version 2027.1.1
<PackageReference Include="WriteableFileProviders.Physical" Version="2027.1.1" />
<PackageVersion Include="WriteableFileProviders.Physical" Version="2027.1.1" />
<PackageReference Include="WriteableFileProviders.Physical" />
paket add WriteableFileProviders.Physical --version 2027.1.1
#r "nuget: WriteableFileProviders.Physical, 2027.1.1"
#:package WriteableFileProviders.Physical@2027.1.1
#addin nuget:?package=WriteableFileProviders.Physical&version=2027.1.1
#tool nuget:?package=WriteableFileProviders.Physical&version=2027.1.1
WriteableFileProviders.Physical
A folder on the local disk as an IWriteableFileProvider, with atomic writes and change notifications. Part of
WriteableFileProviders.
Install
dotnet add package WriteableFileProviders.Physical
Needs .NET 10, and is native AOT compatible. Keep every WriteableFileProviders package in an app on the same major version.
Use
using WriteableFileProviders;
using WriteableFileProviders.Physical;
await using var files = new WriteablePhysicalFileProvider("/srv/files");
await files.UpdateFileAsync("reports/2026.csv", stream); // creates reports/ if missing
await files.UpdateFileAsync("app.log", line, UpdateFileOptions.Append);
await using var slice = await files.ReadRangeAsync("big.bin", 1_000_000, 4096);
var changed = files.Watch("reports/*.csv");
From configuration, with WriteableFileProviderSerializer.FromConfiguration(section) or
services.AddWriteableFileProvider(section):
{ "Files": { "Type": "WriteablePhysicalFileProvider", "RootDirectoryPath": "/srv/files" } }
Configuration
| Option | Default | Meaning |
|---|---|---|
RootDirectoryPath |
required | The folder the provider is confined to: absolute (/srv/files, C:\files, \\server\share\files) or relative to the current directory, resolved once when the provider is built. It need not exist; the first write creates it. |
RootDirectorySegments |
The same folder as segments, the form 2026 stored: ["root:", "srv", "files"], ["C:", "files"], ["\\", "server", "share"]. Set this or RootDirectoryPath. |
|
RefuseLinks |
false |
Refuse a path that names or goes through a symbolic link, junction or Windows mount point inside the root (ArgumentException), and leave links out of listings. The root itself may be a link. New in 2027.1.0: an older version ignores it and follows links. |
Authentication
None. The provider reads and writes with the permissions of the process.
What's different
- Writes are atomic. An overwrite, an append, a copy or a local stream's commit writes
.wfp-<guid>.tmpnext to the target, then renames it over the target. Listings hide these files; a crash can leave one.DeleteTempFilesAsync(olderThan)deletes those older than an age you choose, which must be longer than your longest write, plus any lag of a file server's clock: a write whose temp file goes fails, and its file keeps its content. It doesn't walk into linked folders, and skips a folder below the root that it may not list; a root it can't read throws. - An append copies the whole file, so frequent appends to a large file (a log) are expensive. It first copies your
stream to a hidden
.wfp-<guid>.tmpbeside the file (on Linux and macOS readable by the process's user only), so that after a concurrent write it can read the file again and append after it, up to five attempts in all. On Linux and macOS, an append that loses a new name to another writer still throwsFileAlreadyExistsExceptionwhile the file there is empty (a move's destination, a file system without hard links, or a file another writer left empty): it can't tell such a file from one a rename is about to fill, which would undo the append. On Windows, which doesn't rename over a file that is open, one of two appends to one file at once can fail withUnauthorizedAccessException. - A write puts a new file at the path. Other hard links keep the old content, and a symbolic link at the path becomes a file. On Windows the file takes the folder's permissions; on Linux and macOS, the old file's mode bits. ACLs and extended attributes are not kept.
IfMatchis not atomic. It is checked just before the rename. ETags come from the last-write time and length.- On Windows, a write to a file that is open for reading fails (
UnauthorizedAccessExceptionorIOException), and the file keeps its content. Linux and macOS let the write go ahead. - Case is ignored on Windows and macOS, and matters elsewhere.
:is refused in names everywhere. On Windows, so are names Windows can't hold or would read as another name:report.txt.,a?b,CON,con.txt. Listings leave out such names, and on Linux and macOS, names holding:or\. - Symbolic links and junctions inside the root are followed, wherever they point, unless you set
RefuseLinks. Without it, a recursive listing lists a linked folder but doesn't walk into it.RefuseLinkschecks the path just before each operation, so a process that can create links inside the root can still swap one in between, except for a folder delete on Linux (x64, Arm, Arm64), macOS and Windows, which opens every folder from the root without following a link. Hard links and mounts on Linux and macOS aren't visible as links, and other reparse points (cloud placeholders, Windows container layers) aren't links. The pollingWatchcan still report a change behind a linked folder. - A folder delete never leaves the folder on Linux (x64, Arm, Arm64), macOS and Windows. It opens each entry in its
folder without following a link and removes it there, so a link inside, even one swapped in while it runs, is
removed as itself. It goes on past what it can't delete and throws the first failure; cancelled, it stops between
entries, the folder partly deleted. On Windows a folder holding a junction is deleted. On macOS the folder that holds
the deleted one, and with
RefuseLinksevery folder on the way, must be readable. Elsewhere (Linux on other architectures, other systems) the delete goes by path, as .NET's does, and a link swapped in while it runs can still steer it outside the folder. - No content types.
UpdateFileOptions.ContentTypeis ignored;ContentTypeis null. Watchworks like ASP.NET Core'sPhysicalFileProvider: a glob relative to the root. Hidden and dot-prefixed files aren't watched. Where file system events don't arrive (some network and container mounts), setDOTNET_USE_POLLING_FILE_WATCHER=1.- No native signed URLs.
GetSignedUrlAsyncthrowsNotSupportedException. For download and upload links, use the gateway in WriteableFileProviders.AspNetCore.
Learn more
- Getting started: local files first, then download links through the gateway.
- The guide: reading and writing, paths and listings, errors, providers compared.
- Moving from 2026.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.FileProviders.Physical (>= 10.0.12)
- WriteableFileProviders.Common (>= 2027.1.1 && < 2028.0.0-alpha)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2027.1.1 | 43 | 10/13/2026 |
| 2027.1.1-rc.1 | 42 | 10/13/2026 |
| 2027.1.0 | 73 | 10/12/2026 |
| 2027.1.0-rc.1 | 49 | 10/12/2026 |
| 2027.0.0 | 360 | 10/11/2026 |
| 2027.0.0-rc.2 | 63 | 10/11/2026 |
What each release changed: https://github.com/serionist/WriteableFileProviders/blob/main/CHANGELOG.md. Moving from 2026.x: https://github.com/serionist/WriteableFileProviders/blob/main/docs/migration-2027.md