Bennewitz.Ninja.FileServer
2026.9.23
Prefix Reserved
dotnet add package Bennewitz.Ninja.FileServer --version 2026.9.23
NuGet\Install-Package Bennewitz.Ninja.FileServer -Version 2026.9.23
<PackageReference Include="Bennewitz.Ninja.FileServer" Version="2026.9.23" />
<PackageVersion Include="Bennewitz.Ninja.FileServer" Version="2026.9.23" />
<PackageReference Include="Bennewitz.Ninja.FileServer" />
paket add Bennewitz.Ninja.FileServer --version 2026.9.23
#r "nuget: Bennewitz.Ninja.FileServer, 2026.9.23"
#:package Bennewitz.Ninja.FileServer@2026.9.23
#addin nuget:?package=Bennewitz.Ninja.FileServer&version=2026.9.23
#tool nuget:?package=Bennewitz.Ninja.FileServer&version=2026.9.23
Bennewitz.Ninja.FileServer
Mount a browsable, downloadable view of a directory onto a route of an ASP.NET Core application. Directory listings and rendered Markdown, styled out of the box, with downloads covered by whatever authorization you put on the mount.
dotnet add package Bennewitz.Ninja.FileServer
Quick start
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFileServer();
var app = builder.Build();
app.MapFileServer("/docs", options =>
{
options.RootPath = "/srv/docs"; // absolute, must exist at startup
});
app.Run();
/docs now lists the directory, /docs/guide/setup.md renders as HTML, and
/docs/guide/setup.md?raw serves the source. Nothing else is required: the views, the
stylesheet, and the colour-scheme script are compiled or embedded into the assembly and served
from the mount's own endpoint, so the component works in a host that never calls
UseStaticFiles and in a single-file publish.
Protecting a mount
MapFileServer returns the mount's route group, so a convention applied to it covers every
route the mount owns — listings and file downloads alike:
app.MapFileServer("/private", options => options.RootPath = "/srv/private")
.RequireAuthorization("StaffOnly");
Files are served from endpoints rather than static-file middleware precisely for this reason: static-file middleware produces no endpoints, so authorization would have nothing to enforce against and downloads would stay open while listings looked protected. The stylesheet endpoint is deliberately left anonymous, so the login page a challenge redirects to is still styled.
Several directories at once
Each mount is configured and protected independently, with no shared state:
app.MapFileServer("/public", o => o.RootPath = "/srv/public");
app.MapFileServer("/reports", o =>
{
o.RootPath = "/srv/reports";
o.AllowedExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".pdf", ".csv" };
}).RequireAuthorization();
Registrations that would make authorization ambiguous fail while the pipeline is being built, not at request time: a duplicate prefix, or a root that overlaps another mount's root.
Options
| Option | Default | Purpose |
|---|---|---|
RootPath |
(required) | Absolute path of the directory to serve. Must exist at startup. |
AllowedExtensions |
(empty — all files) | Extensions that may be listed and downloaded, e.g. .pdf. The leading dot is optional — pdf and .pdf are equivalent. An empty string matches files with no extension. Applied on both paths. |
UnlistedPatterns |
(empty) | Globs for files and directories left out of listings but still served at their exact URL. See Unlisted files. |
ExposedSensitivePatterns |
(empty) | Globs for dot-prefixed, Hidden or System paths to serve anyway, e.g. .well-known/**. See Hidden and dot-prefixed files. |
EnableDirectoryBrowsing |
true |
When false, directories 404 while direct file downloads still work. |
RenderMarkdown |
true |
When false, .md files are served as raw bytes. |
LayoutPath |
null |
A host layout to render inside, e.g. /Views/Shared/_Layout.cshtml. Defaults to the component's own self-contained layout. |
IncludeDefaultStyles |
true |
Emit the component's stylesheet and colour-scheme toggle. Set false to style the markup yourself. |
CacheControl |
no-store |
Cache-Control sent with served files. |
Unlisted files
options.UnlistedPatterns = ["**/*.key", "private"];
A matching entry is left out of its directory's listing and still downloads at its exact URL. Patterns are globs anchored at the mount root and compared case-insensitively:
| Pattern | Matches | Does not match |
|---|---|---|
*.key |
a.key |
sub/a.key |
**/*.key |
a.key, sub/a.key |
|
drafts/*.md |
drafts/a.md |
a.md |
private |
the private directory entry |
what is inside it |
private/** |
everything inside private |
the private directory entry |
An unlisted directory still lists its own contents when you open its URL, unless they match a
pattern too. Unlisting never changes what is served: AllowedExtensions still refuses what it
refuses, and hidden or dot-prefixed files stay refused.
Unlisted is not access control. Anyone holding the URL gets the file, and URLs leak through
browser history, referrers and logs. Protect anything that matters with RequireAuthorization().
Hidden and dot-prefixed files
A path is never listed and never served when any segment below the mount root starts with a dot,
or, on Windows, names a file or directory with the Hidden or System attribute. So .env,
.git/config and everything beneath .private/ return 404. The check runs on the canonical
path, so encoded dot segments, backslashes, alternate data streams and 8.3 short names reach no
further than the plain name would.
To serve one anyway, name it in ExposedSensitivePatterns:
options.ExposedSensitivePatterns = [".well-known/**"];
These patterns match the whole path relative to the root, case-sensitively, since they widen
what is served. .well-known/** serves everything beneath .well-known, dotfiles included, but
not the directory itself; add .well-known as well to make it listable. An exposed path is then
treated like any other: listed unless it is unlisted, and served only if AllowedExtensions
allows it.
Markdown
.md files render as HTML, with ?raw serving the source. Listings and documents alike carry an
Auto / Light / Dark control that pins a colour scheme against the reader's system preference,
remembered under one key so it holds across both. Fenced code blocks are tokenised
into GitHub's own token classes — pl-k, pl-s, pl-c and the rest — which the bundled
stylesheet already colours, so highlighted code follows the colour-scheme toggle rather than
carrying colours of its own. Roughly two dozen languages are recognised (csharp, xml,
javascript, typescript, powershell, sql, python, java, cpp, json, css, html
and friends); a fence tagged with anything else keeps its text and loses only the colour.
Set RenderMarkdown = false to serve .md files as bytes like any other file.
Using your own layout
app.MapFileServer("/docs", options =>
{
options.RootPath = "/srv/docs";
options.LayoutPath = "/Views/Shared/_Layout.cshtml";
});
The component's views never declare Razor sections, so a layout is under no obligation to
render any — an unrendered declared section throws at request time, and a component cannot
know what a host layout renders. Its stylesheet link is emitted in the page body for the same
reason. Every class is prefixed bnfs-, so the styles cannot collide with a host's own
framework, and dropping them (IncludeDefaultStyles = false) leaves the markup intact for you
to style.
Containment
Requests are resolved against the mount root with every path segment's symlinks resolved
first, then compared ordinally. Path.GetFullPath alone is string canonicalisation that never
touches the filesystem, so a link anywhere along the path — not just at the leaf — would
otherwise escape the root undetected.
Standalone server
The same component powers a standalone single-file binary for six platforms, configured by
settings.json, environment variables, or CLI arguments, with Docker images and HTTPS
support. See the repository.
License
MIT.
This package embeds github-markdown-css by Sindre Sorhus for rendered-Markdown styling, used under its MIT licence. The notice travels inside the package as THIRD-PARTY-NOTICES.md.
| 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
- ColorCode.HTML (>= 2.0.15)
- Markdig (>= 1.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.