Runic.Assets.AspNetCore
0.12.1
dotnet add package Runic.Assets.AspNetCore --version 0.12.1
NuGet\Install-Package Runic.Assets.AspNetCore -Version 0.12.1
<PackageReference Include="Runic.Assets.AspNetCore" Version="0.12.1" />
<PackageVersion Include="Runic.Assets.AspNetCore" Version="0.12.1" />
<PackageReference Include="Runic.Assets.AspNetCore" />
paket add Runic.Assets.AspNetCore --version 0.12.1
#r "nuget: Runic.Assets.AspNetCore, 0.12.1"
#:package Runic.Assets.AspNetCore@0.12.1
#addin nuget:?package=Runic.Assets.AspNetCore&version=0.12.1
#tool nuget:?package=Runic.Assets.AspNetCore&version=0.12.1
Runic.Assets.AspNetCore
Serve a Runic Assets manifest through ASP.NET Core GET and HEAD endpoints without recreating its media types, cache policy, content lengths, or entity tags.
Install
dotnet add package Runic.Assets.AspNetCore --prerelease
The package targets .NET 10, uses the shared Microsoft.AspNetCore.App
framework, and brings Runic.Assets with it. It is currently a preview package.
Choose this adapter for ASP.NET Core; choose the core package alone when you
only need an asset source or archive.
Map an embedded Vite build
After configuring RunicAssetsDist in the web application's project and
building the Vite dist directory, map the source in Program.cs:
using System.Reflection;
using Runic.Assets;
using Runic.Assets.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
AssetArchiveSource assets = AssetArchive.ReadEmbedded(
Assembly.GetExecutingAssembly());
app.MapRunicAssetSource(assets);
app.Run();
This serves the Vite entry point at / and /index.html, every manifest path
at its exact URL, and the entry point for missing paths without a file extension
(such as /settings/profile), so client-side routes survive a reload. A path
with a trailing slash, such as /settings/, is not an asset path and is not
served.
The endpoint matches only GET and HEAD requests for paths that resolve this
way. It runs after the application's own endpoints, including controller routes,
and before MapFallback. Other requests reach static files, a fallback or a
404.
With the fallback on, an unknown extensionless GET below the mount, such as
/api/typo, returns the entry point with status 200. If the same application
serves an API, mount the assets under a prefix or turn the fallback off. Add an
optional prefix when assets should live below a route:
app.MapRunicAssetSource(assets, "ui");
/ui and /ui/ both serve the entry point. Relative URLs in the document
resolve against / when it is requested as /ui, so build the frontend with an
absolute base, such as Vite's base: "/ui/".
The Desktop adapter in Runic.Assets.Desktop resolves paths with the same
AssetManifest.TryResolveRequestPath rules and defaults, so one archive routes
identically on both hosts. The hosts differ outside the adapters: Runic Desktop
answers other methods with 405 and decodes %2F in request paths, while
ASP.NET Core leaves both to the application. Pass AssetRoutingOptions to
change the rules:
// Only exact manifest paths; the root and unknown paths return 404.
app.MapRunicAssetSource(assets, routing: new AssetRoutingOptions
{
ServeEntryPointAtRoot = false,
EnableSinglePageApplicationFallback = false,
});
The adapter accepts any IAssetSource, which opens each asset's descriptor and
stream together: AssetArchiveSource, EmbeddedAssetSource,
DevelopmentDirectoryAssetSource or your own.
To serve an asset from your own endpoint, resolve it and call the
HttpContext extension WriteAssetAsync, which writes the same headers and
handles conditional and range requests:
app.MapGet("/download/{name}", async (HttpContext context, string name) =>
{
if (assets.Manifest.TryGetAsset("downloads/" + name, out AssetDescriptor? asset))
await context.WriteAssetAsync(assets, asset);
else
context.Response.StatusCode = StatusCodes.Status404NotFound;
});
A minimal-API handler can return RunicAssetResults.Asset instead. The path
resolves with the same routing rules when the result runs, against the source's
current manifest, and a path that does not resolve is 404:
app.MapGet("/app/{**path}", (string? path) => RunicAssetResults.Asset(assets, path ?? ""));
Content-Security-Policy
Set AssetRoutingOptions.ContentSecurityPolicy to send a policy with every
HTML asset; scripts, styles and other assets are sent without it. The archive
carries no policy, because only the application knows its script and connect
origins:
app.MapRunicAssetSource(assets, routing: new AssetRoutingOptions
{
ContentSecurityPolicy = "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
});
MapRunicAssetSource, RunicAssetResults.Asset and the Desktop adapter send
it; WriteAssetAsync does not. A value with a line break or another control
character throws ArgumentException.
Static files middleware
RunicAssetFileProvider exposes a source as an IFileProvider, for
UseStaticFiles, UseDefaultFiles, UseDirectoryBrowser or another consumer
of file providers:
var files = new RunicAssetFileProvider(assets);
app.UseDefaultFiles(new DefaultFilesOptions { FileProvider = files });
app.UseStaticFiles(new StaticFileOptions { FileProvider = files });
Its files are the declared assets and its directories are their parents. A
file has no physical path, and its last-modified time is the archive format's
fixed timestamp (1980-01-01). CreateReadStream waits for the source's
OpenAsync: archive, embedded and development-directory sources open at once,
and another source blocks the request thread. Watch fires on the next change
of a source that implements IAssetSourceChangeNotifier, such as
DevelopmentDirectoryAssetSource, whatever the filter.
The static files middleware computes its own entity tags from the length and
the timestamp, and applies neither the manifest's cache modes nor a
Content-Security-Policy. Prefer MapRunicAssetSource when nothing else needs a
file provider.
HTTP behavior
Responses preserve the manifest-owned content type, length, cache-control
value, and strong ETag, and include X-Content-Type-Options: nosniff. text/*
content types without a charset are sent with ; charset=utf-8, as the Desktop
adapter sends them, so one archive gets the same Content-Type on both hosts. HEAD returns the same
headers without a body.
Matching If-None-Match values receive 304 Not Modified. Unknown and invalid
paths that the routing options do not send to the entry point return 404; the
adapter never serves files outside the manifest.
Documentation and support
| 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
- Runic.Assets (= 0.12.1)
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 |
|---|---|---|
| 0.12.1 | 69 | 10/27/2026 |
| 0.12.0 | 158 | 10/27/2026 |
| 0.11.0 | 1,313 | 10/25/2026 |
| 0.10.0 | 2,801 | 10/22/2026 |
| 0.9.1 | 397 | 10/21/2026 |
| 0.9.0 | 124 | 10/21/2026 |
| 0.8.0-preview.6 | 443 | 10/19/2026 |
| 0.8.0-preview.5 | 143 | 10/17/2026 |
| 0.8.0-preview.4 | 393 | 10/17/2026 |
| 0.8.0-preview.3 | 398 | 10/15/2026 |
| 0.8.0-preview.2 | 81 | 10/15/2026 |
| 0.8.0-preview.1 | 141 | 10/15/2026 |
| 0.7.0-preview.9 | 328 | 10/14/2026 |
| 0.7.0-preview.8 | 82 | 10/12/2026 |
| 0.7.0-preview.7 | 58 | 10/12/2026 |
| 0.7.0-preview.6 | 353 | 10/9/2026 |
| 0.7.0-preview.5 | 125 | 10/9/2026 |
| 0.7.0-preview.4 | 75 | 10/8/2026 |
| 0.7.0-preview.3 | 62 | 10/8/2026 |
| 0.7.0-preview.2 | 68 | 10/7/2026 |