Runic Assets turns a frontend build, such as Vite's dist folder, into one
validated archive inside your .NET application, and serves it from ASP.NET Core
or a Runic Desktop window. Every file keeps its media type, length, SHA-256
digest, entity tag and cache policy, so both hosts answer the same request the
same way.
Use it when the application should ship as one executable instead of an
executable and a folder, or when an ASP.NET Core application serves a single
page application. Projects created with dotnet new runic-app do not need it:
they copy the built frontend to a www folder next to the executable.
This guide follows Runic SDK 0.7.0-preview.1. Differences from 0.6.0-preview.1 are marked where they occur.
Replace <VERSION> below with the current release from the
package catalog. The code below is quoted
from the package READMEs and checked against them.
1. Add the packages
dotnet add package Runic.Assets --version <VERSION>
Add the adapter for the host that serves the files:
dotnet add package Runic.Assets.AspNetCore --version <VERSION>
# or, for Runic Desktop windows:
dotnet add package Runic.Assets.Desktop --version <VERSION>
Runic.Assets has no UI or web framework dependency. Each adapter brings it
along.
2. Embed the frontend build
Build the frontend first, for example with npm run build, so its output
folder exists. Then point the .NET project at that folder:
<PropertyGroup>
<RunicAssetsDist>../Client.Web/dist</RunicAssetsDist>
</PropertyGroup>
dotnet build packs the folder into a canonical archive and embeds it as the
Runic.Assets.StaticFiles resource. The build skips packing when the files and
the project look unchanged by their timestamps, so after deleting a file from
the folder run a rebuild. The same files always produce the same bytes. Load it at
startup:
using System.Reflection;
using Runic.Assets;
AssetArchiveSource assets = AssetArchive.ReadEmbedded(
Assembly.GetExecutingAssembly());
| Property | Use |
|---|---|
RunicAssetsDist |
Folder to pack and embed. |
RunicAssetsEntryPoint |
Entry document inside the folder. Defaults to index.html. |
RunicAssetsDistExclude |
Paths to leave out, separated by semicolons. Defaults to runic-assets.zip; setting it replaces the default, so list that file again if the folder contains it. |
RunicAssetsEmbeddedArchive |
Embed an archive that was packed in a separate step (see below) instead of RunicAssetsDist. |
RunicAssetsEmbeddedResourceName |
Resource name, if not Runic.Assets.StaticFiles. Pass the same name to ReadEmbedded. |
Hashed file names, such as Vite's index-Cf3tzbYH.js, are cached as immutable.
Every other file, including index.html, is revalidated with its entity tag.
3. Serve it from ASP.NET Core
Map the source in Program.cs:
AssetArchiveSource assets = AssetArchive.ReadEmbedded(
Assembly.GetExecutingAssembly());
app.MapRunicAssetSource(assets);
app.Run();
The endpoint serves every file at its path, the entry point at /, and the
entry point for missing paths without a file extension, such as
/settings/profile, so client-side routes survive a reload. It answers only
GET and HEAD, supports conditional and range requests, and runs after the
application's own endpoints.
Because unknown extensionless paths return the entry point, an application that
also serves an API should mount the assets under a prefix and build the
frontend with the same absolute base (Vite's base: "/ui/"):
app.MapRunicAssetSource(assets, "ui");
or serve exact paths only:
app.MapRunicAssetSource(assets, routing: new AssetRoutingOptions
{
ServeEntryPointAtRoot = false,
EnableSinglePageApplicationFallback = false,
});
In 0.6.0-preview.1 MapRunicAssetSource serves exact manifest paths only, so
the entry point is at /index.html and there is no AssetRoutingOptions.
4. Serve it in a Runic Desktop window
Pass the adapter's content handler as the surface content:
AssetArchiveSource assets = AssetArchive.ReadEmbedded(Assembly.GetExecutingAssembly());
await using var host = await DesktopHost.StartAsync();
await using var surface = await host.CreateSurfaceAsync(new DesktopSurfaceOptions
{
Content = new DesktopContent.Handler(assets.ToDesktopContentHandler()),
});
Paths resolve exactly as in ASP.NET Core, so one archive behaves the same on
both hosts. Pass AssetRoutingOptions to ToDesktopContentHandler to change
the rules.
In 0.6.0-preview.1 set ContentHandler = assets.ToDesktopContentHandler()
instead of Content. There, ToDesktopContentHandler extends IAssetSource
and takes DesktopAssetOptions? options; in 0.7 it extends
IAssetSnapshotSource and takes AssetRoutingOptions? routing.
5. Pack in a separate step
Most applications let dotnet build pack the folder. Run the packer yourself
when the archive is produced in a separate step: a CI job that builds the
frontend once for several .NET builds, an application that loads an archive
file at run time, or to inspect exactly what the build would embed. The packer
ships inside the Runic.Assets package:
packer="$HOME/.nuget/packages/runic.assets/<VERSION>/tools/net10.0/Runic.Assets.Packer.dll"
dotnet "$packer" Client.Web/dist artifacts/app.runic-assets --trusted-generated-output
Embed the result with RunicAssetsEmbeddedArchive, or open the file at run time
with AssetArchive.Read. The
packer README
lists its options, JSON output and exit codes.
Trusted generated output
Without --trusted-generated-output, the packer opens every file and folder
through pinned Linux handles, so a file swapped for a symbolic link while it
runs cannot redirect it outside the source folder. That mode requires Linux.
With --trusted-generated-output the packer uses ordinary file APIs and works
on Linux, Windows and macOS. It still rejects symbolic links and reparse points
it sees, but assumes that nothing changes the folder while it runs. Use it for
output of your own build or CI job that no other process can write to; the
dotnet build integration always uses it for that reason. Leave it off on Linux
when a less trusted process can write to the folder, such as a shared upload
directory.
Troubleshooting
The RunicAssetsDist directory '...' does not exist.The frontend was not built beforedotnet build. Build it first, or make the build depend on it.Set either RunicAssetsDist or RunicAssetsEmbeddedArchive, not both.Choose one source for the embedded archive.Entry point '...' does not exist below '...' or was excluded.(packer exit code4) SetRunicAssetsEntryPointor--entry-pointto a file in the folder that is not excluded.Directory archive compilation requires Linux handle-pinned traversal.(packer exit code5) Without--trusted-generated-outputthe packer runs only on Linux. Pass the option for your own build output on Windows and macOS.- A request for
/or a client route returns404on 0.6.0-preview.1. 0.6 serves exact paths only; request/index.htmlor upgrade.
Reference
Runic.Assets: explicit embedded resources, the Linux development directory source, validation and inspection.Runic.Assets.AspNetCoreandRunic.Assets.Desktop: HTTP behavior and routing.- Archive format and the framework-neutral asset boundary.