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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Bennewitz.Ninja.FileServer" Version="2026.9.23" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Bennewitz.Ninja.FileServer" Version="2026.9.23" />
                    
Directory.Packages.props
<PackageReference Include="Bennewitz.Ninja.FileServer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Bennewitz.Ninja.FileServer --version 2026.9.23
                    
#r "nuget: Bennewitz.Ninja.FileServer, 2026.9.23"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Bennewitz.Ninja.FileServer@2026.9.23
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Bennewitz.Ninja.FileServer&version=2026.9.23
                    
Install as a Cake Addin
#tool nuget:?package=Bennewitz.Ninja.FileServer&version=2026.9.23
                    
Install as a Cake Tool

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
2026.9.23 66 9/23/2026
2026.9.2 174 9/2/2026
2026.8.19 124 8/19/2026
2026.8.18 119 8/18/2026